Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ use core::ops::ControlFlow;
use crate::{
ecmascript::{
Agent, ArgumentsList, Array, BUILTIN_STRING_MEMORY, BuiltinConstructorFunction,
ECMAScriptCodeEvaluationState, Environment, ExceptionType, ExecutionContext, Function,
InternalMethods, InternalSlots, IteratorRecord, JsError, JsResult, KeyedGroup, Number,
Object, OrdinaryObject, PrivateName, PropertyDescriptor, PropertyKey, PropertyKeySet,
PropertyLookupCache, ProtoIntrinsics, Realm, SetResult, SmallInteger, String, TryError,
TryGetResult, TryHasResult, TryResult, Value, array_create,
ECMAScriptCodeEvaluationState, ECMAScriptFunction, Environment, ExceptionType,
ExecutionContext, Function, InternalMethods, InternalSlots, IteratorRecord, JsError,
JsResult, KeyedGroup, Number, Object, OrdinaryObject, PrivateName, PropertyDescriptor,
PropertyKey, PropertyKeySet, PropertyLookupCache, ProtoIntrinsics, Realm, SetResult,
SmallInteger, String, TryError, TryGetResult, TryHasResult, TryResult, Value, array_create,
canonicalize_keyed_collection_key, get_iterator, if_abrupt_close_iterator, is_callable,
is_constructor, iterator_close_with_error, iterator_step_value, js_result_into_try,
new_class_field_initializer_environment, require_object_coercible, to_length, to_object,
Expand Down Expand Up @@ -2747,6 +2747,59 @@ pub(crate) fn initialize_instance_elements<'a>(
Ok(())
}

/// Runs the deferred class field initializer bytecode associated with a
/// user-written ECMAScript function constructor.
///
/// For a user-written derived class constructor that has instance fields
/// declared on the class, the field initializers must not run before
/// `super()` (because `this` is uninitialized at that point). The compiler
/// stores them as a separate executable on the function. This helper runs
/// that executable in a new function environment where `this` is bound to
/// the constructed instance, mirroring the behaviour of
/// [`initialize_instance_elements`] for built-in default constructors.
pub(crate) fn initialize_ecmascript_function_class_field_initializers<'a>(
agent: &mut Agent,
f: ECMAScriptFunction,
instance: Object,
gc: GcScope<'a, '_>,
) -> JsResult<'a, ()> {
// Read everything we need before mutating the agent.
let bytecode = f.get(agent).class_field_initializer_bytecode;
let bytecode = match bytecode {
Some(b) => b.unbind(),
None => return Ok(()),
};
let f = f.bind(gc.nogc());
let outer_env = f.get(agent).ecmascript_function.environment;
let outer_priv_env = f.get(agent).ecmascript_function.private_environment;
let source_code = f.get(agent).ecmascript_function.source_code;
let realm = f.get(agent).ecmascript_function.realm;
let instance = instance.bind(gc.nogc());
let decl_env = new_class_field_initializer_environment(
agent,
Function::ECMAScriptFunction(f),
instance,
outer_env,
gc.nogc(),
);
agent.push_execution_context(ExecutionContext {
ecmascript_code: Some(ECMAScriptCodeEvaluationState {
lexical_environment: Environment::Function(decl_env.unbind()),
variable_environment: Environment::Function(decl_env.unbind()),
private_environment: outer_priv_env.unbind(),
is_strict_mode: true,
source_code: source_code.unbind(),
}),
function: Some(Function::ECMAScriptFunction(f.unbind())),
realm: realm.unbind(),
script_or_module: None,
});
let bytecode = bytecode.scope(agent, gc.nogc());
let result = Vm::execute(agent, bytecode, None, gc).into_js_result();
agent.pop_execution_context();
result.map(|_| ())
}

/// ### [7.3.34 AddValueToKeyedGroup ( groups, key, value )](https://tc39.es/ecma262/#sec-add-value-to-keyed-group)
/// The abstract operation AddValueToKeyedGroup takes arguments groups (a List of Records with fields
/// [[Key]] (an ECMAScript language value) and [[Elements]] (a List of ECMAScript language values)),
Expand Down
5 changes: 5 additions & 0 deletions nova_vm/src/ecmascript/builtins/ecmascript_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ pub(crate) fn ordinary_function_create<'gc>(
ecmascript_function,
compiled_bytecode: None,
name: None,
class_field_initializer_bytecode: None,
};
if let Some(function_prototype) = params.function_prototype
&& function_prototype
Expand Down Expand Up @@ -1226,6 +1227,7 @@ impl HeapMarkAndSweep for ECMAScriptFunctionHeapData<'static> {
ecmascript_function,
compiled_bytecode,
name,
class_field_initializer_bytecode,
} = self;
let ECMAScriptFunctionObjectHeapData {
environment,
Expand All @@ -1243,6 +1245,7 @@ impl HeapMarkAndSweep for ECMAScriptFunctionHeapData<'static> {
object_index.mark_values(queues);
compiled_bytecode.mark_values(queues);
name.mark_values(queues);
class_field_initializer_bytecode.mark_values(queues);
environment.mark_values(queues);
private_environment.mark_values(queues);
realm.mark_values(queues);
Expand All @@ -1258,6 +1261,7 @@ impl HeapMarkAndSweep for ECMAScriptFunctionHeapData<'static> {
ecmascript_function,
compiled_bytecode,
name,
class_field_initializer_bytecode,
} = self;
let ECMAScriptFunctionObjectHeapData {
environment,
Expand All @@ -1275,6 +1279,7 @@ impl HeapMarkAndSweep for ECMAScriptFunctionHeapData<'static> {
object_index.sweep_values(compactions);
compiled_bytecode.sweep_values(compactions);
name.sweep_values(compactions);
class_field_initializer_bytecode.sweep_values(compactions);
environment.sweep_values(compactions);
private_environment.sweep_values(compactions);
realm.sweep_values(compactions);
Expand Down
4 changes: 4 additions & 0 deletions nova_vm/src/ecmascript/types/language/function/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ pub(crate) struct ECMAScriptFunctionHeapData<'a> {
/// Stores the compiled bytecode of an ECMAScript function.
pub(crate) compiled_bytecode: Option<Executable<'a>>,
pub(crate) name: Option<String<'a>>,
/// For a user-written derived class constructor with instance fields,
/// holds the compiled bytecode that initializes those fields. It is run
/// after `super()` has bound `this` (from `EvaluateSuper` step 11).
pub(crate) class_field_initializer_bytecode: Option<Executable<'a>>,
}

unsafe impl Send for ECMAScriptFunctionHeapData<'_> {}
Expand Down
3 changes: 3 additions & 0 deletions nova_vm/src/engine/bytecode/bytecode_compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,7 @@ impl<'a, 's, 'gc, 'scope> CompileEvaluation<'a, 's, 'gc, 'scope> for ast::Functi
}),
identifier,
compiled_bytecode: None,
class_field_initializer_bytecode: None,
},
);
}
Expand Down Expand Up @@ -1436,6 +1437,7 @@ impl<'a, 's, 'gc, 'scope> CompileEvaluation<'a, 's, 'gc, 'scope> for ast::Object
}),
identifier,
compiled_bytecode: None,
class_field_initializer_bytecode: None,
},
// enumerable: true,
true.into(),
Expand Down Expand Up @@ -1484,6 +1486,7 @@ impl<'a, 's, 'gc, 'scope> CompileEvaluation<'a, 's, 'gc, 'scope> for ast::Object
}),
identifier: None,
compiled_bytecode: None,
class_field_initializer_bytecode: None,
},
// enumerable: true,
true.into(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,16 +636,44 @@ impl<'a, 's, 'gc, 'scope> CompileEvaluation<'a, 's, 'gc, 'scope> for ast::Class<
constructor_ctx.add_instruction(Instruction::Store);
let source_code = constructor_ctx.get_source_code();
if let Some(constructor) = constructor {
let constructor_data = CompileFunctionBodyData {
source_code,
is_lexical: false,
// Class code is always strict.
is_strict: true,
ast: FunctionAstRef::ClassConstructor(&constructor.value),
};
constructor_ctx.compile_function_body(constructor_data);
let executable = constructor_ctx.finish();
ctx.set_function_expression_bytecode(constructor_index, executable);
// For a user-written constructor on a derived class, the
// instance field initializers cannot run before `super()`
// because `this` is uninitialized at that point. Build the
// prelude as a separate executable and register it so it
// runs from `EvaluateSuper` step 11 after `super()` has
// bound `this`. For base classes the existing
// prelude-inside-body approach is preserved because
// `OrdinaryCallBindThis` runs before the user body and so
// `this` is already initialized.
if has_constructor_parent {
let initializer_executable = constructor_ctx.finish();
let mut body_ctx = CompileContext::new(agent, source_code, gc);
let constructor_data = CompileFunctionBodyData {
source_code,
is_lexical: false,
// Class code is always strict.
is_strict: true,
ast: FunctionAstRef::ClassConstructor(&constructor.value),
};
body_ctx.compile_function_body(constructor_data);
let body_executable = body_ctx.finish();
ctx.set_function_expression_class_field_initializer_bytecode(
constructor_index,
initializer_executable,
);
ctx.set_function_expression_bytecode(constructor_index, body_executable);
} else {
let constructor_data = CompileFunctionBodyData {
source_code,
is_lexical: false,
// Class code is always strict.
is_strict: true,
ast: FunctionAstRef::ClassConstructor(&constructor.value),
};
constructor_ctx.compile_function_body(constructor_data);
let executable = constructor_ctx.finish();
ctx.set_function_expression_bytecode(constructor_index, executable);
}
} else {
let executable = constructor_ctx.finish();
ctx.add_class_initializer_bytecode(executable, has_constructor_parent);
Expand Down Expand Up @@ -854,6 +882,7 @@ fn define_constructor_method(
// CompileContext holds a name identifier for us if this is NamedEvaluation.
identifier: None,
compiled_bytecode: None,
class_field_initializer_bytecode: None,
},
has_constructor_parent.into(),
)
Expand Down Expand Up @@ -915,6 +944,7 @@ fn define_method<'s>(
// Note: method name is always found in the result register.
identifier: Some(NamedEvaluationParameter::Result),
compiled_bytecode: None,
class_field_initializer_bytecode: None,
},
// enumerable: false,
false.into(),
Expand Down Expand Up @@ -998,6 +1028,7 @@ fn define_private_method<'s>(
}),
identifier: Some(NamedEvaluationParameter::Result),
compiled_bytecode: None,
class_field_initializer_bytecode: None,
},
immediate.into(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,15 @@ impl<'agent, 'script, 'gc, 'scope> CompileContext<'agent, 'script, 'gc, 'scope>
.set_function_expression_bytecode(index, executable);
}

pub(super) fn set_function_expression_class_field_initializer_bytecode(
&mut self,
index: IndexType,
executable: Executable<'gc>,
) {
self.executable
.set_function_expression_class_field_initializer_bytecode(index, executable);
}

pub(super) fn add_class_initializer_bytecode(
&mut self,
executable: Executable<'gc>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,15 @@ impl<'agent, 'gc, 'scope> ExecutableContext<'agent, 'gc, 'scope> {
self.function_expressions[index as usize].compiled_bytecode = Some(executable);
}

pub(super) fn set_function_expression_class_field_initializer_bytecode(
&mut self,
index: IndexType,
executable: Executable<'gc>,
) {
self.function_expressions[index as usize].class_field_initializer_bytecode =
Some(executable);
}

pub(super) fn add_class_initializer_bytecode(
&mut self,
executable: Executable<'gc>,
Expand Down
9 changes: 9 additions & 0 deletions nova_vm/src/engine/bytecode/executable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ pub(crate) struct FunctionExpression<'a> {
pub(crate) identifier: Option<NamedEvaluationParameter>,
/// Optionally eagerly compile the FunctionExpression into bytecode.
pub(crate) compiled_bytecode: Option<Executable<'a>>,
/// For a class constructor with instance fields defined on a derived
/// class, holds a separate executable that runs the field initializers
/// after `super()` has bound `this`. The executable is invoked from
/// `EvaluateSuper` step 11 (InitializeInstanceElements).
pub(crate) class_field_initializer_bytecode: Option<Executable<'a>>,
}

bindable_handle!(FunctionExpression);
Expand All @@ -74,17 +79,21 @@ impl HeapMarkAndSweep for FunctionExpression<'static> {
expression: _,
identifier: _,
compiled_bytecode,
class_field_initializer_bytecode,
} = self;
compiled_bytecode.mark_values(queues);
class_field_initializer_bytecode.mark_values(queues);
}

fn sweep_values(&mut self, compactions: &CompactionLists) {
let Self {
expression: _,
identifier: _,
compiled_bytecode,
class_field_initializer_bytecode,
} = self;
compiled_bytecode.sweep_values(compactions);
class_field_initializer_bytecode.sweep_values(compactions);
}
}

Expand Down
49 changes: 34 additions & 15 deletions nova_vm/src/engine/bytecode/vm/execute_instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ use crate::{
copy_data_properties, copy_data_properties_into_object, create_builtin_constructor,
create_data_property_or_throw, create_unmapped_arguments_object, define_property_or_throw,
evaluate_import_call, get_this_environment, get_this_value, get_value, has_property,
is_constructor, is_less_than, is_loosely_equal, is_private_reference,
is_property_reference, is_strictly_equal, is_super_reference, is_unresolvable_reference,
iterator_complete, iterator_value, make_constructor, make_method,
new_class_static_element_environment, new_declarative_environment, new_private_environment,
ordinary_function_create, ordinary_object_create_with_intrinsics, perform_eval,
private_element_find, put_value, resolve_binding, resolve_private_identifier,
resolve_this_binding, set, set_function_name, throw_no_proxy_private_names,
throw_read_undefined_or_null_error, to_boolean, to_number, to_number_primitive, to_numeric,
to_numeric_primitive, to_object, to_property_key, to_property_key_complex,
to_property_key_primitive, to_property_key_simple, to_string, to_string_primitive,
try_copy_data_properties_into_object, try_create_data_property,
initialize_ecmascript_function_class_field_initializers, is_constructor, is_less_than,
is_loosely_equal, is_private_reference, is_property_reference, is_strictly_equal,
is_super_reference, is_unresolvable_reference, iterator_complete, iterator_value,
make_constructor, make_method, new_class_static_element_environment,
new_declarative_environment, new_private_environment, ordinary_function_create,
ordinary_object_create_with_intrinsics, perform_eval, private_element_find, put_value,
resolve_binding, resolve_private_identifier, resolve_this_binding, set, set_function_name,
throw_no_proxy_private_names, throw_read_undefined_or_null_error, to_boolean, to_number,
to_number_primitive, to_numeric, to_numeric_primitive, to_object, to_property_key,
to_property_key_complex, to_property_key_primitive, to_property_key_simple, to_string,
to_string_primitive, try_copy_data_properties_into_object, try_create_data_property,
try_define_property_or_throw, try_get_value, try_has_property,
try_initialize_referenced_binding, try_put_value, try_resolve_binding, try_result_into_js,
try_result_into_option_js, unwrap_try,
Expand Down Expand Up @@ -1220,10 +1220,12 @@ pub(super) fn execute_class_define_constructor<'gc>(
let FunctionExpression {
expression,
compiled_bytecode,
class_field_initializer_bytecode,
..
} = executable.fetch_function_expression(agent, instr.get_first_index(), gc.nogc());
let function_expression = expression.get();
let compiled_bytecode = *compiled_bytecode;
let class_field_initializer_bytecode = *class_field_initializer_bytecode;
let has_constructor_parent = instr.get_second_bool();

let function_prototype = if has_constructor_parent {
Expand Down Expand Up @@ -1252,6 +1254,10 @@ pub(super) fn execute_class_define_constructor<'gc>(
if let Some(compiled_bytecode) = compiled_bytecode {
function.get_mut(agent).compiled_bytecode = Some(compiled_bytecode.unbind());
}
if let Some(class_field_initializer_bytecode) = class_field_initializer_bytecode {
function.get_mut(agent).class_field_initializer_bytecode =
Some(class_field_initializer_bytecode.unbind());
}
set_function_name(agent, function, class_name.into(), None, gc.nogc());
make_constructor(agent, function, Some(false), Some(proto), gc.nogc());
function.get_mut(agent).ecmascript_function.home_object = Some(proto.into());
Expand Down Expand Up @@ -1766,7 +1772,8 @@ pub(super) fn execute_evaluate_super<'gc>(
result.unbind().bind(gc.nogc())
};
// 7. Let thisER be GetThisEnvironment().
let Environment::Function(this_er) = get_this_environment(agent, gc.nogc()) else {
let this_er = get_this_environment(agent, gc.nogc());
let Environment::Function(this_er) = this_er else {
unreachable!();
};
// 8. Perform ? thisER.BindThisValue(result).
Expand All @@ -1776,12 +1783,24 @@ pub(super) fn execute_evaluate_super<'gc>(
.bind(gc.nogc());
// 9. Let F be thisER.[[FunctionObject]].
// 10. Assert: F is an ECMAScript function object.
let Function::ECMAScriptFunction(_f) = this_er.get_function_object(agent) else {
unreachable!();
let f_unbound = match this_er.get_function_object(agent) {
Function::ECMAScriptFunction(f) => f.unbind(),
_ => unreachable!(),
};
// 11. Perform ? InitializeInstanceElements(result, F).
// For a user-written derived class constructor with instance fields
// declared on the class, the field initializers must run after `super()`
// has bound `this`. They are stored on the function as a separate
// executable and invoked here.
let result_object_unbound = result.unbind();
initialize_ecmascript_function_class_field_initializers(
agent,
f_unbound,
result_object_unbound,
gc,
)?;
// 12. Return result.
vm.result = Some(result.unbind().into());
vm.result = Some(result_object_unbound.into());
Ok(())
}

Expand Down
Loading