Browse Source

Refactor the environment for runtime performance (#1829)

This is an attempt to refactor the environments to be more performant at runtime. The idea is, to shift the dynamic hashmap environment lookups from runtime to compile time.

Currently the environments hold hashmaps that contain binding identifiers, values and additional information that is needed to identify some errors. Because bindings in outer environments are accessible from inner environments, this can lead to a traversal through all environments (in the worst case to the global environment).

This change to the environment structure pushes most of the work that is needed to access bindings to the compile time. At compile time, environments and bindings in the environments are being assigned indices. These indices are then stored instead of the `Sym` that is currently used to access bindings. At runtime, the indices are used to access bindings in a fixed size `Vec` per environment. This brings multiple benefits:
 - No hashmap access needed at runtime
 - The number of bindings per environment is known at compile time. Environments only need a single allocation, as their size is constant.
 - Potential for optimizations with `unsafe` https://doc.rust-lang.org/std/vec/struct.Vec.html#method.get_unchecked

Additionally, this changes the global object to have it's bindings directly stored on the `Realm`. This should reduce some overhead from access trough gc objects and makes some optimizations for the global object possible.

The benchmarks look not that great on the first sight. But if you look closer, I think it is apparent, that this is a positive change. The difference is most apparent on Mini and Clean as they are longer (still not near any real life js but less specific that most other benchmarks):

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Clean js (Compiler) | **1929.1±5.37ns** | 4.1±0.02µs | **+112.53%** |
| Clean js (Execution) | 1487.4±7.50µs | **987.3±3.78µs** | **-33.62%** |

The compile time is up in all benchmarks, as expected. The percentage is huge, but if we look at the real numbers, we can see that this is an issue of orders of magnitude. While compile is up `112.53%`, the real change is `~+2µs`. Execution is only down `33.62%`, but the real time changed by `~-500µs`.

Co-authored-by: Iban Eguia <razican@protonmail.ch>
pull/1850/head
raskad 3 years ago
parent
commit
29cd909f88
  1. 2
      boa/benches/full.rs
  2. 196
      boa/src/builtins/function/arguments.rs
  3. 7
      boa/src/builtins/function/mod.rs
  4. 2
      boa/src/builtins/global_this/mod.rs
  5. 8
      boa/src/builtins/mod.rs
  6. 38
      boa/src/builtins/number/mod.rs
  7. 2
      boa/src/builtins/regexp/mod.rs
  8. 2
      boa/src/builtins/set/mod.rs
  9. 864
      boa/src/bytecompiler.rs
  10. 2
      boa/src/class.rs
  11. 118
      boa/src/context.rs
  12. 343
      boa/src/environment/declarative_environment_record.rs
  13. 222
      boa/src/environment/environment_record_trait.rs
  14. 282
      boa/src/environment/function_environment_record.rs
  15. 571
      boa/src/environment/global_environment_record.rs
  16. 259
      boa/src/environment/lexical_environment.rs
  17. 8
      boa/src/environment/mod.rs
  18. 281
      boa/src/environment/object_environment_record.rs
  19. 315
      boa/src/environments/compile.rs
  20. 36
      boa/src/environments/mod.rs
  21. 332
      boa/src/environments/runtime.rs
  22. 92
      boa/src/environments/tests.rs
  23. 2
      boa/src/lib.rs
  24. 460
      boa/src/object/internal_methods/global.rs
  25. 1
      boa/src/object/internal_methods/mod.rs
  26. 3
      boa/src/object/mod.rs
  27. 14
      boa/src/object/property_map.rs
  28. 8
      boa/src/profiler.rs
  29. 52
      boa/src/realm.rs
  30. 70
      boa/src/vm/call_frame.rs
  31. 250
      boa/src/vm/code_block.rs
  32. 397
      boa/src/vm/mod.rs
  33. 41
      boa/src/vm/opcode.rs
  34. 2
      boa_tester/src/exec/js262.rs
  35. 9
      boa_tester/src/exec/mod.rs

2
boa/benches/full.rs

@ -47,7 +47,7 @@ macro_rules! full_benchmarks {
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
let statement_list = context.parse(CODE).expect("parsing failed");
let code_block = context.compile(&statement_list);
let code_block = context.compile(&statement_list).unwrap();
c.bench_function(concat!($id, " (Execution)"), move |b| {
b.iter(|| {
context.execute(black_box(code_block.clone())).unwrap()

196
boa/src/builtins/function/arguments.rs

@ -1,14 +1,15 @@
use crate::{
builtins::Array,
environment::lexical_environment::Environment,
environments::DeclarativeEnvironment,
gc::{Finalize, Trace},
object::{FunctionBuilder, JsObject, ObjectData},
property::PropertyDescriptor,
property::{PropertyDescriptor, PropertyKey},
symbol::{self, WellKnownSymbols},
syntax::ast::node::FormalParameter,
Context, JsValue,
};
use rustc_hash::FxHashSet;
use gc::Gc;
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Trace, Finalize)]
pub struct MappedArguments(JsObject);
@ -57,7 +58,7 @@ impl Arguments {
.configurable(true),
context,
)
.expect("DefinePropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// 5. Let index be 0.
// 6. Repeat, while index < len,
@ -65,7 +66,7 @@ impl Arguments {
// a. Let val be argumentsList[index].
// b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val).
obj.create_data_property_or_throw(index, value, context)
.expect("CreateDataPropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// c. Set index to index + 1.
}
@ -82,7 +83,7 @@ impl Arguments {
.configurable(true),
context,
)
.expect("DefinePropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
let throw_type_error = context.intrinsics().throw_type_error();
@ -98,7 +99,7 @@ impl Arguments {
.configurable(false),
context,
)
.expect("DefinePropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// 9. Return obj.
obj
@ -111,7 +112,7 @@ impl Arguments {
func: &JsObject,
formals: &[FormalParameter],
arguments_list: &[JsValue],
env: &Environment,
env: &Gc<DeclarativeEnvironment>,
context: &mut Context,
) -> JsObject {
// 1. Assert: formals does not contain a rest parameter, any binding patterns, or any initializers.
@ -142,7 +143,7 @@ impl Arguments {
// a. Let val be argumentsList[index].
// b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val).
obj.create_data_property_or_throw(index, val, context)
.expect("CreateDataPropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// c. Set index to index + 1.
}
@ -157,90 +158,105 @@ impl Arguments {
.configurable(true),
context,
)
.expect("DefinePropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// 17. Let mappedNames be a new empty List.
// using a set to optimize `contains`
let mut mapped_names = FxHashSet::default();
// The section 17-19 differs from the spec, due to the way the runtime environments work.
//
// This section creates getters and setters for all mapped arguments.
// Getting and setting values on the `arguments` object will actually access the bindings in the environment:
// ```
// function f(a) {console.log(a); arguments[0] = 1; console.log(a)};
// f(0) // 0, 1
// ```
//
// The spec assumes, that identifiers are used at runtime to reference bindings in the environment.
// We use indices to access environment bindings at runtime.
// To map to function parameters to binding indices, we use the fact, that bindings in a
// function environment start with all of the arguments in order:
// `function f (a,b,c)`
// | binding index | `arguments` property key | identifier |
// | 0 | 0 | a |
// | 1 | 1 | b |
// | 2 | 2 | c |
//
// Notice that the binding index does not correspond to the argument index:
// `function f (a,a,b)` => binding indices 0 (a), 1 (b), 2 (c)
// | binding index | `arguments` property key | identifier |
// | - | 0 | - |
// | 0 | 1 | a |
// | 1 | 2 | b |
// While the `arguments` object contains all arguments, they must not be all bound.
// In the case of duplicate parameter names, the last one is bound as the environment binding.
//
// The following logic implements the steps 17-19 adjusted for our environment structure.
// 12. Let parameterNames be the BoundNames of formals.
// 13. Let numberOfParameters be the number of elements in parameterNames.
// 18. Set index to numberOfParameters - 1.
// 19. Repeat, while index ≥ 0,
// a. Let name be parameterNames[index].
for (index, parameter_name_vec) in
formals.iter().map(FormalParameter::names).enumerate().rev()
{
for parameter_name in parameter_name_vec.iter().copied() {
// b. If name is not an element of mappedNames, then
if !mapped_names.contains(&parameter_name) {
// i. Add name as an element of the list mappedNames.
mapped_names.insert(parameter_name);
// ii. If index < len, then
if index < len {
// 1. Let g be MakeArgGetter(name, env).
// https://tc39.es/ecma262/#sec-makearggetter
let g = {
// 2. Let getter be ! CreateBuiltinFunction(getterClosure, 0, "", « »).
// 3. NOTE: getter is never directly accessible to ECMAScript code.
// 4. Return getter.
FunctionBuilder::closure_with_captures(
context,
// 1. Let getterClosure be a new Abstract Closure with no parameters that captures
// name and env and performs the following steps when called:
|_, _, captures, context| {
captures.0.get_binding_value(captures.1, false, context)
},
(env.clone(), parameter_name),
)
.length(0)
.name("")
.build()
};
// 2. Let p be MakeArgSetter(name, env).
// https://tc39.es/ecma262/#sec-makeargsetter
let p = {
// 2. Let setter be ! CreateBuiltinFunction(setterClosure, 1, "", « »).
// 3. NOTE: setter is never directly accessible to ECMAScript code.
// 4. Return setter.
FunctionBuilder::closure_with_captures(
context,
// 1. Let setterClosure be a new Abstract Closure with parameters (value) that captures
// name and env and performs the following steps when called:
|_, args, captures, context| {
let value = args.get(0).cloned().unwrap_or_default();
// a. Return env.SetMutableBinding(name, value, false).
captures
.0
.set_mutable_binding(captures.1, value, false, context)
.map(|_| JsValue::Undefined)
// Ok(JsValue::Undefined)
},
(env.clone(), parameter_name),
)
.length(1)
.name("")
.build()
};
// 3. Perform map.[[DefineOwnProperty]](! ToString(𝔽(index)), PropertyDescriptor {
// [[Set]]: p, [[Get]]: g, [[Enumerable]]: false, [[Configurable]]: true }).
map.__define_own_property__(
index.into(),
PropertyDescriptor::builder()
.set(p)
.get(g)
.enumerable(false)
.configurable(true)
.build(),
context,
)
.expect("[[DefineOwnProperty]] must not fail per the spec");
}
let mut bindings = FxHashMap::default();
let mut property_index = 0;
'outer: for formal in formals {
for name in formal.names() {
if property_index >= len {
break 'outer;
}
let binding_index = bindings.len() + 1;
let entry = bindings
.entry(name)
.or_insert((binding_index, property_index));
entry.1 = property_index;
property_index += 1;
}
}
for (binding_index, property_index) in bindings.values() {
// 19.b.ii.1. Let g be MakeArgGetter(name, env).
// https://tc39.es/ecma262/#sec-makearggetter
let g = {
// 2. Let getter be ! CreateBuiltinFunction(getterClosure, 0, "", « »).
// 3. NOTE: getter is never directly accessible to ECMAScript code.
// 4. Return getter.
FunctionBuilder::closure_with_captures(
context,
// 1. Let getterClosure be a new Abstract Closure with no parameters that captures
// name and env and performs the following steps when called:
|_, _, captures, _| Ok(captures.0.get(captures.1)),
(env.clone(), *binding_index),
)
.length(0)
.build()
};
// 19.b.ii.2. Let p be MakeArgSetter(name, env).
// https://tc39.es/ecma262/#sec-makeargsetter
let p = {
// 2. Let setter be ! CreateBuiltinFunction(setterClosure, 1, "", « »).
// 3. NOTE: setter is never directly accessible to ECMAScript code.
// 4. Return setter.
FunctionBuilder::closure_with_captures(
context,
// 1. Let setterClosure be a new Abstract Closure with parameters (value) that captures
// name and env and performs the following steps when called:
|_, args, captures, _| {
let value = args.get(0).cloned().unwrap_or_default();
captures.0.set(captures.1, value);
Ok(JsValue::undefined())
},
(env.clone(), *binding_index),
)
.length(1)
.build()
};
// 19.b.ii.3. Perform map.[[DefineOwnProperty]](! ToString(𝔽(index)), PropertyDescriptor {
// [[Set]]: p, [[Get]]: g, [[Enumerable]]: false, [[Configurable]]: true }).
map.__define_own_property__(
PropertyKey::from(*property_index),
PropertyDescriptor::builder()
.set(p)
.get(g)
.enumerable(false)
.configurable(true)
.build(),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
}
// 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor {
// [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false,
@ -254,7 +270,7 @@ impl Arguments {
.configurable(true),
context,
)
.expect("DefinePropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor {
// [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
@ -267,7 +283,7 @@ impl Arguments {
.configurable(true),
context,
)
.expect("DefinePropertyOrThrow must not fail per the spec");
.expect("Defining new own properties for a new ordinary object cannot fail");
// 22. Return obj.
obj

7
boa/src/builtins/function/mod.rs

@ -11,11 +11,10 @@
//! [spec]: https://tc39.es/ecma262/#sec-function-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
use super::JsArgs;
use crate::{
builtins::BuiltIn,
builtins::{BuiltIn, JsArgs},
context::StandardObjects,
environment::lexical_environment::Environment,
environments::DeclarativeEnvironmentStack,
gc::{self, Finalize, Gc, Trace},
object::{
internal_methods::get_prototype_from_constructor, JsObject, NativeObject, Object,
@ -177,7 +176,7 @@ pub enum Function {
},
VmOrdinary {
code: Gc<crate::vm::CodeBlock>,
environment: Environment,
environments: DeclarativeEnvironmentStack,
},
}

2
boa/src/builtins/global_this/mod.rs

@ -28,6 +28,6 @@ impl BuiltIn for GlobalThis {
fn init(context: &mut Context) -> JsValue {
let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
context.global_object().into()
context.global_object().clone().into()
}
}

8
boa/src/builtins/mod.rs

@ -101,11 +101,11 @@ fn init_builtin<B: BuiltIn>(context: &mut Context) {
.value(value)
.writable(B::ATTRIBUTE.writable())
.enumerable(B::ATTRIBUTE.enumerable())
.configurable(B::ATTRIBUTE.configurable());
.configurable(B::ATTRIBUTE.configurable())
.build();
context
.global_object()
.borrow_mut()
.insert(B::NAME, property);
.global_bindings_mut()
.insert(B::NAME.into(), property);
}
/// Initializes built-in objects and functions

38
boa/src/builtins/number/mod.rs

@ -13,13 +13,12 @@
//! [spec]: https://tc39.es/ecma262/#sec-number-object
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
use super::string::is_trimmable_whitespace;
use super::JsArgs;
use crate::context::StandardObjects;
use crate::object::JsObject;
use crate::{
builtins::{function::make_builtin_fn, BuiltIn},
object::{internal_methods::get_prototype_from_constructor, ConstructorBuilder, ObjectData},
builtins::{string::is_trimmable_whitespace, BuiltIn, JsArgs},
context::StandardObjects,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
value::{AbstractRelation, IntegerOrInfinity, JsValue},
BoaProfiler, Context, JsResult,
@ -39,12 +38,6 @@ const BUF_SIZE: usize = 2200;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Number;
/// Maximum number of arguments expected to the builtin parseInt() function.
const PARSE_INT_MAX_ARG_COUNT: usize = 2;
/// Maximum number of arguments expected to the builtin parseFloat() function.
const PARSE_FLOAT_MAX_ARG_COUNT: usize = 1;
impl BuiltIn for Number {
const NAME: &'static str = "Number";
@ -83,23 +76,10 @@ impl BuiltIn for Number {
.static_method(Self::number_is_integer, "isInteger", 1)
.build();
let global = context.global_object();
make_builtin_fn(
Self::parse_int,
"parseInt",
&global,
PARSE_INT_MAX_ARG_COUNT,
context,
);
make_builtin_fn(
Self::parse_float,
"parseFloat",
&global,
PARSE_FLOAT_MAX_ARG_COUNT,
context,
);
make_builtin_fn(Self::global_is_finite, "isFinite", &global, 1, context);
make_builtin_fn(Self::global_is_nan, "isNaN", &global, 1, context);
context.register_global_builtin_function("parseInt", 2, Self::parse_int);
context.register_global_builtin_function("parseFloat", 1, Self::parse_float);
context.register_global_builtin_function("isFinite", 1, Self::global_is_finite);
context.register_global_builtin_function("isNaN", 1, Self::global_is_nan);
number_object.into()
}

2
boa/src/builtins/regexp/mod.rs

@ -308,7 +308,7 @@ impl RegExp {
pub(crate) fn create(p: JsValue, f: JsValue, context: &mut Context) -> JsResult<JsValue> {
// 1. Let obj be ? RegExpAlloc(%RegExp%).
let obj = Self::alloc(
&context.global_object().get(Self::NAME, context)?,
&context.global_object().clone().get(Self::NAME, context)?,
&[],
context,
)?;

2
boa/src/builtins/set/mod.rs

@ -330,7 +330,7 @@ impl Set {
let this_arg = args.get_or_undefined(1);
// TODO: if condition should also check that we are not in strict mode
let this_arg = if this_arg.is_undefined() {
JsValue::Object(context.global_object())
context.global_object().clone().into()
} else {
this_arg.clone()
};

864
boa/src/bytecompiler.rs

File diff suppressed because it is too large Load Diff

2
boa/src/class.rs

@ -110,7 +110,7 @@ impl<T: Class> ClassConstructor for T {
));
}
let class_constructor = context.global_object().get(T::NAME, context)?;
let class_constructor = context.global_object().clone().get(T::NAME, context)?;
let class_constructor = if let JsValue::Object(ref obj) = class_constructor {
obj
} else {

118
boa/src/context.rs

@ -1,7 +1,5 @@
//! Javascript context.
use boa_interner::Sym;
use crate::{
builtins::{
self, function::NativeFunctionSignature, intrinsics::IntrinsicObjects,
@ -10,13 +8,14 @@ use crate::{
bytecompiler::ByteCompiler,
class::{Class, ClassBuilder},
gc::Gc,
object::{FunctionBuilder, JsObject, ObjectData},
object::{FunctionBuilder, GlobalPropertyMap, JsObject, ObjectData},
property::{Attribute, PropertyDescriptor, PropertyKey},
realm::Realm,
syntax::{ast::node::StatementList, parser::ParseError, Parser},
vm::{CallFrame, CodeBlock, FinallyReturn, Vm},
BoaProfiler, Interner, JsResult, JsValue,
};
use boa_interner::Sym;
#[cfg(feature = "console")]
use crate::builtins::console::Console;
@ -513,8 +512,20 @@ impl Context {
/// Return the global object.
#[inline]
pub fn global_object(&self) -> JsObject {
self.realm.global_object.clone()
pub fn global_object(&self) -> &JsObject {
self.realm.global_object()
}
/// Return a reference to the global object string bindings.
#[inline]
pub(crate) fn global_bindings(&self) -> &GlobalPropertyMap {
self.realm.global_bindings()
}
/// Return a mutable reference to the global object string bindings.
#[inline]
pub(crate) fn global_bindings_mut(&mut self) -> &mut GlobalPropertyMap {
self.realm.global_bindings_mut()
}
/// Constructs a `Error` with the specified message.
@ -719,22 +730,59 @@ impl Context {
name: &str,
length: usize,
body: NativeFunctionSignature,
) -> JsResult<()> {
) {
let function = FunctionBuilder::native(self, body)
.name(name)
.length(length)
.constructor(true)
.build();
self.global_object().insert_property(
name,
self.global_bindings_mut().insert(
name.into(),
PropertyDescriptor::builder()
.value(function)
.writable(true)
.enumerable(false)
.configurable(true),
.configurable(true)
.build(),
);
}
/// Register a global native function that is not a constructor.
///
/// This is more efficient that creating a closure function, since this does not allocate,
/// it is just a function pointer.
///
/// The function will be bound to the global object with `writable`, `non-enumerable`
/// and `configurable` attributes. The same as when you create a function in JavaScript.
///
/// # Note
///
/// The difference to [`Context::register_global_function`](Context::register_global_function) is,
/// that the function will not be `constructable`.
/// Usage of the function as a constructor will produce a `TypeError`.
#[inline]
pub fn register_global_builtin_function(
&mut self,
name: &str,
length: usize,
body: NativeFunctionSignature,
) {
let function = FunctionBuilder::native(self, body)
.name(name)
.length(length)
.constructor(false)
.build();
self.global_bindings_mut().insert(
name.into(),
PropertyDescriptor::builder()
.value(function)
.writable(true)
.enumerable(false)
.configurable(true)
.build(),
);
Ok(())
}
/// Register a global closure function.
@ -769,13 +817,14 @@ impl Context {
.constructor(true)
.build();
self.global_object().insert_property(
name,
self.global_bindings_mut().insert(
name.into(),
PropertyDescriptor::builder()
.value(function)
.writable(true)
.enumerable(false)
.configurable(true),
.configurable(true)
.build(),
);
Ok(())
}
@ -816,8 +865,10 @@ impl Context {
.value(class)
.writable(T::ATTRIBUTES.writable())
.enumerable(T::ATTRIBUTES.enumerable())
.configurable(T::ATTRIBUTES.configurable());
self.global_object().insert(T::NAME, property);
.configurable(T::ATTRIBUTES.configurable())
.build();
self.global_bindings_mut().insert(T::NAME.into(), property);
Ok(())
}
@ -859,13 +910,14 @@ impl Context {
K: Into<PropertyKey>,
V: Into<JsValue>,
{
self.global_object().insert(
key,
self.realm.global_property_map.insert(
&key.into(),
PropertyDescriptor::builder()
.value(value)
.writable(attribute.writable())
.enumerable(attribute.enumerable())
.configurable(attribute.configurable()),
.configurable(attribute.configurable())
.build(),
);
}
@ -897,7 +949,7 @@ impl Context {
Err(e) => return self.throw_syntax_error(e),
};
let code_block = self.compile(&statement_list);
let code_block = self.compile(&statement_list)?;
let result = self.execute(code_block);
// The main_timer needs to be dropped before the BoaProfiler is.
@ -909,11 +961,14 @@ impl Context {
/// Compile the AST into a `CodeBlock` ready to be executed by the VM.
#[inline]
pub fn compile(&mut self, statement_list: &StatementList) -> Gc<CodeBlock> {
let _ = BoaProfiler::global().start_event("Compilation", "Main");
let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), &self.interner);
compiler.compile_statement_list(statement_list, true);
Gc::new(compiler.finish())
pub fn compile(&mut self, statement_list: &StatementList) -> JsResult<Gc<CodeBlock>> {
let _timer = BoaProfiler::global().start_event("Compilation", "Main");
let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), self);
for node in statement_list.items() {
compiler.create_declarations(node)?;
}
compiler.compile_statement_list(statement_list, true)?;
Ok(Gc::new(compiler.finish()))
}
/// Call the VM with a `CodeBlock` and return the result.
@ -924,8 +979,8 @@ impl Context {
/// `Gc<CodeBlock>` returned by the [`Self::compile()`] function.
#[inline]
pub fn execute(&mut self, code_block: Gc<CodeBlock>) -> JsResult<JsValue> {
let _ = BoaProfiler::global().start_event("Execution", "Main");
let global_object = self.global_object().into();
let _timer = BoaProfiler::global().start_event("Execution", "Main");
let global_object = self.global_object().clone().into();
self.vm.push_frame(CallFrame {
prev: None,
@ -936,12 +991,19 @@ impl Context {
finally_return: FinallyReturn::None,
finally_jump: Vec::new(),
pop_on_return: 0,
pop_env_on_return: 0,
loop_env_stack: vec![0],
try_env_stack: vec![crate::vm::TryStackEntry {
num_env: 0,
num_loop_stack_entries: 0,
}],
param_count: 0,
arg_count: 0,
});
self.run()
self.realm.set_global_binding_number();
let result = self.run();
self.vm.pop_frame();
result
}
/// Return the cached iterator prototypes.

343
boa/src/environment/declarative_environment_record.rs

@ -1,343 +0,0 @@
//! # Declarative Records
//!
//! Each declarative Environment Record is associated with an ECMAScript program scope containing variable,
//! `constant`, `let`, `class`, `module`, `import`, and/or function declarations.
//! A declarative Environment Record binds the set of identifiers defined by the declarations contained within its scope.
//! More info: [ECMA-262 sec-declarative-environment-records](https://tc39.es/ecma262/#sec-declarative-environment-records)
use crate::{
environment::{
environment_record_trait::EnvironmentRecordTrait,
lexical_environment::{Environment, EnvironmentType},
},
gc::{self, Finalize, Gc, Trace},
object::JsObject,
BoaProfiler, Context, JsResult, JsValue,
};
use boa_interner::Sym;
use rustc_hash::FxHashMap;
/// Declarative Bindings have a few properties for book keeping purposes, such as mutability (const vs let).
/// Can it be deleted? and strict mode.
///
/// So we need to create a struct to hold these values.
/// From this point onwards, a binding is referring to one of these structures.
#[derive(Trace, Finalize, Debug, Clone)]
pub struct DeclarativeEnvironmentRecordBinding {
pub value: Option<JsValue>,
pub can_delete: bool,
pub mutable: bool,
pub strict: bool,
}
/// A declarative Environment Record binds the set of identifiers defined by the
/// declarations contained within its scope.
#[derive(Debug, Trace, Finalize, Clone)]
pub struct DeclarativeEnvironmentRecord {
pub env_rec: gc::Cell<FxHashMap<Sym, DeclarativeEnvironmentRecordBinding>>,
pub outer_env: Option<Environment>,
}
impl DeclarativeEnvironmentRecord {
pub fn new(env: Option<Environment>) -> Self {
let _timer = BoaProfiler::global().start_event("new_declarative_environment", "env");
Self {
env_rec: gc::Cell::new(FxHashMap::default()),
outer_env: env,
}
}
}
impl EnvironmentRecordTrait for DeclarativeEnvironmentRecord {
/// `9.1.1.1.1 HasBinding ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-hasbinding-n
fn has_binding(&self, name: Sym, _context: &mut Context) -> JsResult<bool> {
// 1. If envRec has a binding for the name that is the value of N, return true.
// 2. Return false.
Ok(self.env_rec.borrow().contains_key(&name))
}
/// `9.1.1.1.2 CreateMutableBinding ( N, D )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-createmutablebinding-n-d
fn create_mutable_binding(
&self,
name: Sym,
deletion: bool,
allow_name_reuse: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Assert: envRec does not already have a binding for N.
if !allow_name_reuse {
assert!(
!self.env_rec.borrow().contains_key(&name),
"Identifier {} has already been declared",
context.interner().resolve_expect(name)
);
}
// 2. Create a mutable binding in envRec for N and record that it is uninitialized.
// If D is true, record that the newly created binding may be deleted by a subsequent DeleteBinding call.
self.env_rec.borrow_mut().insert(
name,
DeclarativeEnvironmentRecordBinding {
value: None,
can_delete: deletion,
mutable: true,
strict: false,
},
);
// 3. Return NormalCompletion(empty).
Ok(())
}
/// `9.1.1.1.3 CreateImmutableBinding ( N, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-createimmutablebinding-n-s
fn create_immutable_binding(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Assert: envRec does not already have a binding for N.
assert!(
!self.env_rec.borrow().contains_key(&name),
"Identifier {} has already been declared",
context.interner().resolve_expect(name)
);
// 2. Create an immutable binding in envRec for N and record that it is uninitialized.
// If S is true, record that the newly created binding is a strict binding.
self.env_rec.borrow_mut().insert(
name,
DeclarativeEnvironmentRecordBinding {
value: None,
can_delete: true,
mutable: false,
strict,
},
);
// 3. Return NormalCompletion(empty).
Ok(())
}
/// `9.1.1.1.4 InitializeBinding ( N, V )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-initializebinding-n-v
fn initialize_binding(&self, name: Sym, value: JsValue, context: &mut Context) -> JsResult<()> {
if let Some(ref mut record) = self.env_rec.borrow_mut().get_mut(&name) {
if record.value.is_none() {
// 2. Set the bound value for N in envRec to V.
// 3. Record that the binding for N in envRec has been initialized.
record.value = Some(value);
// 4. Return NormalCompletion(empty).
return Ok(());
}
}
// 1. Assert: envRec must have an uninitialized binding for N.
panic!(
"record must have binding for {}",
context.interner().resolve_expect(name)
);
}
/// `9.1.1.1.5 SetMutableBinding ( N, V, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-setmutablebinding-n-v-s
#[allow(clippy::else_if_without_else)]
fn set_mutable_binding(
&self,
name: Sym,
value: JsValue,
mut strict: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. If envRec does not have a binding for N, then
if self.env_rec.borrow().get(&name).is_none() {
// a. If S is true, throw a ReferenceError exception.
if strict {
return context.throw_reference_error(format!(
"{} not found",
context.interner().resolve_expect(name)
));
}
// b. Perform envRec.CreateMutableBinding(N, true).
self.create_mutable_binding(name, true, false, context)?;
// c. Perform envRec.InitializeBinding(N, V).
self.initialize_binding(name, value, context)?;
// d. Return NormalCompletion(empty).
return Ok(());
}
let (binding_strict, binding_value_is_none, binding_mutable) = {
let env_rec = self.env_rec.borrow();
let binding = env_rec.get(&name).unwrap();
(binding.strict, binding.value.is_none(), binding.mutable)
};
// 2. If the binding for N in envRec is a strict binding, set S to true.
if binding_strict {
strict = true;
}
// 3. If the binding for N in envRec has not yet been initialized, throw a ReferenceError exception.
if binding_value_is_none {
return context.throw_reference_error(format!(
"{} has not been initialized",
context.interner().resolve_expect(name)
));
// 4. Else if the binding for N in envRec is a mutable binding, change its bound value to V.
} else if binding_mutable {
let mut env_rec = self.env_rec.borrow_mut();
let binding = env_rec.get_mut(&name).unwrap();
binding.value = Some(value);
// 5. Else,
// a. Assert: This is an attempt to change the value of an immutable binding.
// b. If S is true, throw a TypeError exception.
} else if strict {
return context.throw_type_error(format!(
"Cannot mutate an immutable binding {}",
context.interner().resolve_expect(name)
));
}
// 6. Return NormalCompletion(empty).
Ok(())
}
/// `9.1.1.1.6 GetBindingValue ( N, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-getbindingvalue-n-s
fn get_binding_value(
&self,
name: Sym,
_strict: bool,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Assert: envRec has a binding for N.
// 2. If the binding for N in envRec is an uninitialized binding, throw a ReferenceError exception.
// 3. Return the value currently bound to N in envRec.
if let Some(binding) = self.env_rec.borrow().get(&name) {
if let Some(ref val) = binding.value {
Ok(val.clone())
} else {
context.throw_reference_error(format!(
"{} is an uninitialized binding",
context.interner().resolve_expect(name)
))
}
} else {
panic!(
"Cannot get binding value for {}",
context.interner().resolve_expect(name)
);
}
}
/// `9.1.1.1.7 DeleteBinding ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-deletebinding-n
fn delete_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Assert: envRec has a binding for the name that is the value of N.
// 2. If the binding for N in envRec cannot be deleted, return false.
// 3. Remove the binding for N from envRec.
// 4. Return true.
match self.env_rec.borrow().get(&name) {
Some(binding) => {
if binding.can_delete {
self.env_rec.borrow_mut().remove(&name);
Ok(true)
} else {
Ok(false)
}
}
None => panic!(
"env_rec has no binding for {}",
context.interner().resolve_expect(name)
),
}
}
/// `9.1.1.1.8 HasThisBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-hasthisbinding
fn has_this_binding(&self) -> bool {
// 1. Return false.
false
}
fn get_this_binding(&self, _context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
/// `9.1.1.1.9 HasSuperBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-hassuperbinding
fn has_super_binding(&self) -> bool {
// 1. Return false.
false
}
/// `9.1.1.1.10 WithBaseObject ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-declarative-environment-records-withbaseobject
fn with_base_object(&self) -> Option<JsObject> {
None
}
fn get_outer_environment_ref(&self) -> Option<&Environment> {
self.outer_env.as_ref()
}
fn set_outer_environment(&mut self, env: Environment) {
self.outer_env = Some(env);
}
fn get_environment_type(&self) -> EnvironmentType {
EnvironmentType::Declarative
}
}
impl From<DeclarativeEnvironmentRecord> for Environment {
fn from(env: DeclarativeEnvironmentRecord) -> Self {
Gc::new(Box::new(env))
}
}

222
boa/src/environment/environment_record_trait.rs

@ -1,222 +0,0 @@
//! # Environment Records
//!
//! <https://tc39.es/ecma262/#sec-environment-records>
//! <https://tc39.es/ecma262/#sec-lexical-environments>
//!
//! Some environments are stored as `JSObjects`. This is for GC, i.e we want to keep an environment if a variable is closed-over (a closure is returned).
//! All of the logic to handle scope/environment records are stored in here.
//!
//! There are 5 Environment record kinds. They all have methods in common, these are implemented as a the `EnvironmentRecordTrait`
//!
use crate::{
environment::lexical_environment::VariableScope,
environment::lexical_environment::{Environment, EnvironmentType},
gc::{Finalize, Trace},
object::JsObject,
Context, JsResult, JsValue,
};
use boa_interner::Sym;
use std::fmt::Debug;
/// <https://tc39.es/ecma262/#sec-environment-records>
///
/// In the ECMAScript specification Environment Records are hierachical and have a base class with abstract methods.
/// In this implementation we have a trait which represents the behaviour of all `EnvironmentRecord` types.
pub trait EnvironmentRecordTrait: Debug + Trace + Finalize {
/// Determine if an Environment Record has a binding for the String value N.
/// Return true if it does and false if it does not.
fn has_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool>;
/// Create a new but uninitialized mutable binding in an Environment Record. The String value N is the text of the bound name.
/// If the Boolean argument deletion is true the binding may be subsequently deleted.
///
/// * `allow_name_reuse` - specifies whether or not reusing binding names is allowed.
///
/// Most variable names cannot be reused, but functions in JavaScript are allowed to have multiple
/// paraments with the same name.
fn create_mutable_binding(
&self,
name: Sym,
deletion: bool,
allow_name_reuse: bool,
context: &mut Context,
) -> JsResult<()>;
/// Create a new but uninitialized immutable binding in an Environment Record.
/// The String value N is the text of the bound name.
/// If strict is true then attempts to set it after it has been initialized will always throw an exception,
/// regardless of the strict mode setting of operations that reference that binding.
fn create_immutable_binding(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<()>;
/// Set the value of an already existing but uninitialized binding in an Environment Record.
/// The String value N is the text of the bound name.
/// V is the value for the binding and is a value of any ECMAScript language type.
fn initialize_binding(&self, name: Sym, value: JsValue, context: &mut Context) -> JsResult<()>;
/// Set the value of an already existing mutable binding in an Environment Record.
///
/// The String value `name` is the text of the bound name.
/// value is the `value` for the binding and may be a value of any ECMAScript language type.
/// `S` is a `Boolean` flag. If `strict` is true and the binding cannot be set throw a
/// `TypeError` exception.
fn set_mutable_binding(
&self,
name: Sym,
value: JsValue,
strict: bool,
context: &mut Context,
) -> JsResult<()>;
/// Returns the value of an already existing binding from an Environment Record.
/// The String value N is the text of the bound name.
/// S is used to identify references originating in strict mode code or that
/// otherwise require strict mode reference semantics.
fn get_binding_value(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<JsValue>;
/// Delete a binding from an Environment Record.
/// The String value name is the text of the bound name.
/// If a binding for name exists, remove the binding and return true.
/// If the binding exists but cannot be removed return false. If the binding does not exist return true.
fn delete_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool>;
/// Determine if an Environment Record establishes a this binding.
/// Return true if it does and false if it does not.
fn has_this_binding(&self) -> bool;
/// Return the `this` binding from the environment
fn get_this_binding(&self, context: &mut Context) -> JsResult<JsValue>;
/// Determine if an Environment Record establishes a super method binding.
/// Return true if it does and false if it does not.
fn has_super_binding(&self) -> bool;
/// If this Environment Record is associated with a with statement, return the with object.
/// Otherwise, return None.
fn with_base_object(&self) -> Option<JsObject>;
/// Get the next environment up
fn get_outer_environment_ref(&self) -> Option<&Environment>;
fn get_outer_environment(&self) -> Option<Environment> {
self.get_outer_environment_ref().cloned()
}
/// Set the next environment up
fn set_outer_environment(&mut self, env: Environment);
/// Get the type of environment this is
fn get_environment_type(&self) -> EnvironmentType;
/// Return the `this` binding from the environment or try to get it from outer environments
fn recursive_get_this_binding(&self, context: &mut Context) -> JsResult<JsValue> {
if self.has_this_binding() {
self.get_this_binding(context)
} else {
match self.get_outer_environment_ref() {
Some(outer) => outer.recursive_get_this_binding(context),
None => Ok(JsValue::undefined()),
}
}
}
/// Create mutable binding while handling outer environments
fn recursive_create_mutable_binding(
&self,
name: Sym,
deletion: bool,
scope: VariableScope,
context: &mut Context,
) -> JsResult<()> {
match scope {
VariableScope::Block => self.create_mutable_binding(name, deletion, false, context),
VariableScope::Function => self
.get_outer_environment_ref()
.expect("No function or global environment")
.recursive_create_mutable_binding(name, deletion, scope, context),
}
}
/// Create immutable binding while handling outer environments
fn recursive_create_immutable_binding(
&self,
name: Sym,
deletion: bool,
scope: VariableScope,
context: &mut Context,
) -> JsResult<()> {
match scope {
VariableScope::Block => self.create_immutable_binding(name, deletion, context),
VariableScope::Function => self
.get_outer_environment_ref()
.expect("No function or global environment")
.recursive_create_immutable_binding(name, deletion, scope, context),
}
}
/// Set mutable binding while handling outer environments
fn recursive_set_mutable_binding(
&self,
name: Sym,
value: JsValue,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
if self.has_binding(name, context)? {
self.set_mutable_binding(name, value, strict, context)
} else {
self.get_outer_environment_ref()
.expect("Environment stack underflow")
.recursive_set_mutable_binding(name, value, strict, context)
}
}
/// Initialize binding while handling outer environments
fn recursive_initialize_binding(
&self,
name: Sym,
value: JsValue,
context: &mut Context,
) -> JsResult<()> {
if self.has_binding(name, context)? {
self.initialize_binding(name, value, context)
} else {
self.get_outer_environment_ref()
.expect("Environment stack underflow")
.recursive_initialize_binding(name, value, context)
}
}
/// Check if a binding exists in current or any outer environment
fn recursive_has_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
Ok(self.has_binding(name, context)?
|| match self.get_outer_environment_ref() {
Some(outer) => outer.recursive_has_binding(name, context)?,
None => false,
})
}
/// Retrieve binding from current or any outer environment
fn recursive_get_binding_value(&self, name: Sym, context: &mut Context) -> JsResult<JsValue> {
if self.has_binding(name, context)? {
self.get_binding_value(name, false, context)
} else {
match self.get_outer_environment_ref() {
Some(outer) => outer.recursive_get_binding_value(name, context),
None => context.throw_reference_error(format!(
"{} is not defined",
context.interner().resolve_expect(name)
)),
}
}
}
}

282
boa/src/environment/function_environment_record.rs

@ -1,282 +0,0 @@
//! # Function Environment Records
//!
//! A function Environment Record is a declarative Environment Record that is used to represent
//! the top-level scope of a function and, if the function is not an `ArrowFunction`,
//! provides a `this` binding.
//! If a function is not an `ArrowFunction` function and references super,
//! its function Environment Record also contains the state that is used to perform super method invocations
//! from within the function.
//! More info: <https://tc39.es/ecma262/#sec-function-environment-records>
use crate::{
environment::{
declarative_environment_record::DeclarativeEnvironmentRecord,
environment_record_trait::EnvironmentRecordTrait,
lexical_environment::{Environment, EnvironmentType, VariableScope},
},
gc::{empty_trace, Finalize, Gc, Trace},
object::{JsObject, JsPrototype},
Context, JsResult, JsValue,
};
use boa_interner::Sym;
/// Different binding status for `this`.
/// Usually set on a function environment record
#[derive(Copy, Finalize, Debug, Clone)]
pub enum BindingStatus {
/// If the value is "lexical", this is an ArrowFunction and does not have a local this value.
Lexical,
/// If initialized the function environment record has already been bound with a `this` value
Initialized,
/// If uninitialized the function environment record has not been bouned with a `this` value
Uninitialized,
}
unsafe impl Trace for BindingStatus {
empty_trace!();
}
/// <https://tc39.es/ecma262/#table-16>
#[derive(Debug, Trace, Finalize, Clone)]
pub struct FunctionEnvironmentRecord {
pub declarative_record: DeclarativeEnvironmentRecord,
/// This is the this value used for this invocation of the function.
pub this_value: JsValue,
/// If the value is "lexical", this is an ArrowFunction and does not have a local this value.
pub this_binding_status: BindingStatus,
/// The function object whose invocation caused this Environment Record to be created.
pub function: JsObject,
/// If the associated function has super property accesses and is not an ArrowFunction,
/// `[[HomeObject]]` is the object that the function is bound to as a method.
/// The default value for `[[HomeObject]]` is undefined.
pub home_object: JsValue,
/// If this Environment Record was created by the `[[Construct]]` internal method,
/// `[[NewTarget]]` is the value of the `[[Construct]]` newTarget parameter.
/// Otherwise, its value is undefined.
pub new_target: JsValue,
}
impl FunctionEnvironmentRecord {
pub fn new(
f: JsObject,
this: Option<JsValue>,
outer: Option<Environment>,
binding_status: BindingStatus,
new_target: JsValue,
context: &mut Context,
) -> JsResult<Self> {
let mut func_env = Self {
declarative_record: DeclarativeEnvironmentRecord::new(outer), // the outer environment will come from Environment set as a private property of F - https://tc39.es/ecma262/#sec-ecmascript-function-objects
function: f,
this_binding_status: binding_status,
home_object: JsValue::undefined(),
new_target,
this_value: JsValue::undefined(),
};
// If a `this` value has been passed, bind it to the environment
if let Some(v) = this {
func_env.bind_this_value(v, context)?;
}
Ok(func_env)
}
/// `9.1.1.3.1 BindThisValue ( V )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-bindthisvalue
pub fn bind_this_value(&mut self, value: JsValue, context: &mut Context) -> JsResult<JsValue> {
match self.this_binding_status {
// 1. Assert: envRec.[[ThisBindingStatus]] is not lexical.
BindingStatus::Lexical => {
panic!("Cannot bind to an arrow function!");
}
// 2. If envRec.[[ThisBindingStatus]] is initialized, throw a ReferenceError exception.
BindingStatus::Initialized => {
context.throw_reference_error("Cannot bind to an initialized function!")
}
BindingStatus::Uninitialized => {
// 3. Set envRec.[[ThisValue]] to V.
self.this_value = value.clone();
// 4. Set envRec.[[ThisBindingStatus]] to initialized.
self.this_binding_status = BindingStatus::Initialized;
// 5. Return V.
Ok(value)
}
}
}
/// `9.1.1.3.5 GetSuperBase ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-getsuperbase
pub fn get_super_base(&self, context: &mut Context) -> JsResult<Option<JsPrototype>> {
// 1. Let home be envRec.[[FunctionObject]].[[HomeObject]].
let home = &self.home_object;
// 2. If home has the value undefined, return undefined.
if home.is_undefined() {
Ok(None)
} else {
// 3. Assert: Type(home) is Object.
assert!(home.is_object());
// 4. Return ? home.[[GetPrototypeOf]]().
Ok(Some(
home.as_object()
.expect("home_object must be an Object")
.__get_prototype_of__(context)?,
))
}
}
}
impl EnvironmentRecordTrait for FunctionEnvironmentRecord {
fn has_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
self.declarative_record.has_binding(name, context)
}
fn create_mutable_binding(
&self,
name: Sym,
deletion: bool,
allow_name_reuse: bool,
context: &mut Context,
) -> JsResult<()> {
self.declarative_record
.create_mutable_binding(name, deletion, allow_name_reuse, context)
}
fn create_immutable_binding(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
self.declarative_record
.create_immutable_binding(name, strict, context)
}
fn initialize_binding(&self, name: Sym, value: JsValue, context: &mut Context) -> JsResult<()> {
self.declarative_record
.initialize_binding(name, value, context)
}
fn set_mutable_binding(
&self,
name: Sym,
value: JsValue,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
self.declarative_record
.set_mutable_binding(name, value, strict, context)
}
fn get_binding_value(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<JsValue> {
self.declarative_record
.get_binding_value(name, strict, context)
}
fn delete_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
self.declarative_record.delete_binding(name, context)
}
/// `9.1.1.3.2 HasThisBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-function-environment-records-hasthisbinding
fn has_this_binding(&self) -> bool {
// 1. If envRec.[[ThisBindingStatus]] is lexical, return false; otherwise, return true.
!matches!(self.this_binding_status, BindingStatus::Lexical)
}
/// `9.1.1.3.3 HasSuperBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-function-environment-records-hassuperbinding
fn has_super_binding(&self) -> bool {
// 1. If envRec.[[ThisBindingStatus]] is lexical, return false.
// 2. If envRec.[[FunctionObject]].[[HomeObject]] has the value undefined, return false; otherwise, return true.
if let BindingStatus::Lexical = self.this_binding_status {
false
} else {
!self.home_object.is_undefined()
}
}
/// `9.1.1.3.4 GetThisBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-function-environment-records-getthisbinding
fn get_this_binding(&self, context: &mut Context) -> JsResult<JsValue> {
match self.this_binding_status {
// 1. Assert: envRec.[[ThisBindingStatus]] is not lexical.
BindingStatus::Lexical => {
panic!("There is no this for a lexical function record");
}
// 2. If envRec.[[ThisBindingStatus]] is uninitialized, throw a ReferenceError exception.
BindingStatus::Uninitialized => {
context.throw_reference_error("Uninitialized binding for this function")
}
// 3. Return envRec.[[ThisValue]].
BindingStatus::Initialized => Ok(self.this_value.clone()),
}
}
fn with_base_object(&self) -> Option<JsObject> {
None
}
fn get_outer_environment_ref(&self) -> Option<&Environment> {
self.declarative_record.get_outer_environment_ref()
}
fn set_outer_environment(&mut self, env: Environment) {
self.declarative_record.set_outer_environment(env);
}
fn get_environment_type(&self) -> EnvironmentType {
EnvironmentType::Function
}
fn recursive_create_mutable_binding(
&self,
name: Sym,
deletion: bool,
_scope: VariableScope,
context: &mut Context,
) -> JsResult<()> {
self.create_mutable_binding(name, deletion, false, context)
}
fn recursive_create_immutable_binding(
&self,
name: Sym,
deletion: bool,
_scope: VariableScope,
context: &mut Context,
) -> JsResult<()> {
self.create_immutable_binding(name, deletion, context)
}
}
impl From<FunctionEnvironmentRecord> for Environment {
fn from(env: FunctionEnvironmentRecord) -> Self {
Gc::new(Box::new(env))
}
}

571
boa/src/environment/global_environment_record.rs

@ -1,571 +0,0 @@
//! # Global Environment Records
//!
//! A global Environment Record is used to represent the outer most scope that is shared by all
//! of the ECMAScript Script elements that are processed in a common realm.
//! A global Environment Record provides the bindings for built-in globals (clause 18),
//! properties of the global object, and for all top-level declarations (13.2.8, 13.2.10)
//! that occur within a Script.
//! More info: <https://tc39.es/ecma262/#sec-global-environment-records>
use crate::{
environment::{
declarative_environment_record::DeclarativeEnvironmentRecord,
environment_record_trait::EnvironmentRecordTrait,
lexical_environment::{Environment, EnvironmentType, VariableScope},
object_environment_record::ObjectEnvironmentRecord,
},
gc::{self, Finalize, Gc, Trace},
object::JsObject,
property::PropertyDescriptor,
Context, JsResult, JsValue,
};
use boa_interner::Sym;
use rustc_hash::FxHashSet;
#[derive(Debug, Trace, Finalize, Clone)]
pub struct GlobalEnvironmentRecord {
pub object_record: ObjectEnvironmentRecord,
pub global_this_binding: JsObject,
pub declarative_record: DeclarativeEnvironmentRecord,
pub var_names: gc::Cell<FxHashSet<Sym>>,
}
impl GlobalEnvironmentRecord {
pub fn new(global: JsObject, this_value: JsObject) -> Self {
let obj_rec = ObjectEnvironmentRecord {
bindings: global,
outer_env: None,
/// Object Environment Records created for with statements (13.11)
/// can provide their binding object as an implicit this value for use in function calls.
/// The capability is controlled by a withEnvironment Boolean value that is associated
/// with each object Environment Record. By default, the value of withEnvironment is false
/// for any object Environment Record.
with_environment: false,
};
let dcl_rec = DeclarativeEnvironmentRecord::new(None);
Self {
object_record: obj_rec,
global_this_binding: this_value,
declarative_record: dcl_rec,
var_names: gc::Cell::new(FxHashSet::default()),
}
}
/// `9.1.1.4.12 HasVarDeclaration ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-hasvardeclaration
pub fn has_var_declaration(&self, name: Sym) -> bool {
// 1. Let varDeclaredNames be envRec.[[VarNames]].
// 2. If varDeclaredNames contains N, return true.
// 3. Return false.
self.var_names.borrow().contains(&name)
}
/// `9.1.1.4.13 HasLexicalDeclaration ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-haslexicaldeclaration
pub fn has_lexical_declaration(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. Return DclRec.HasBinding(N).
self.declarative_record.has_binding(name, context)
}
/// `9.1.1.4.14 HasRestrictedGlobalProperty ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-hasrestrictedglobalproperty
pub fn has_restricted_global_property(
&self,
name: Sym,
context: &mut Context,
) -> JsResult<bool> {
// 1. Let ObjRec be envRec.[[ObjectRecord]].
// 2. Let globalObject be ObjRec.[[BindingObject]].
let global_object = &self.object_record.bindings;
// 3. Let existingProp be ? globalObject.[[GetOwnProperty]](N).
let existing_prop = global_object
.__get_own_property__(&context.interner().resolve_expect(name).into(), context)?;
if let Some(existing_prop) = existing_prop {
// 5. If existingProp.[[Configurable]] is true, return false.
// 6. Return true.
Ok(!existing_prop.expect_configurable())
} else {
// 4. If existingProp is undefined, return false.
Ok(false)
}
}
/// `9.1.1.4.15 CanDeclareGlobalVar ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-candeclareglobalvar
pub fn can_declare_global_var(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let ObjRec be envRec.[[ObjectRecord]].
// 2. Let globalObject be ObjRec.[[BindingObject]].
let global_object = &self.object_record.bindings;
// 3. Let hasProperty be ? HasOwnProperty(globalObject, N).
let key = context.interner().resolve_expect(name).to_owned();
let has_property = global_object.has_own_property(key, context)?;
// 4. If hasProperty is true, return true.
if has_property {
return Ok(true);
}
// 5. Return ? IsExtensible(globalObject).
global_object.is_extensible(context)
}
/// `9.1.1.4.16 CanDeclareGlobalFunction ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-candeclareglobalfunction
pub fn can_declare_global_function(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let ObjRec be envRec.[[ObjectRecord]].
// 2. Let globalObject be ObjRec.[[BindingObject]].
let global_object = &self.object_record.bindings;
// 3. Let existingProp be ? globalObject.[[GetOwnProperty]](N).
let existing_prop = global_object
.__get_own_property__(&context.interner().resolve_expect(name).into(), context)?;
if let Some(existing_prop) = existing_prop {
// 5. If existingProp.[[Configurable]] is true, return true.
// 6. If IsDataDescriptor(existingProp) is true and existingProp has attribute values { [[Writable]]: true, [[Enumerable]]: true }, return true.
if existing_prop.expect_configurable()
|| matches!(
(
existing_prop.is_data_descriptor(),
existing_prop.writable(),
existing_prop.enumerable(),
),
(true, Some(true), Some(true))
)
{
Ok(true)
} else {
// 7. Return false.
Ok(false)
}
} else {
// 4. If existingProp is undefined, return ? IsExtensible(globalObject).
global_object.is_extensible(context)
}
}
/// `9.1.1.4.17 CreateGlobalVarBinding ( N, D )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createglobalvarbinding
pub fn create_global_var_binding(
&mut self,
name: Sym,
deletion: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let ObjRec be envRec.[[ObjectRecord]].
// 2. Let globalObject be ObjRec.[[BindingObject]].
let global_object = &self.object_record.bindings;
// 3. Let hasProperty be ? HasOwnProperty(globalObject, N).
let has_property = global_object
.has_own_property(context.interner().resolve_expect(name).to_owned(), context)?;
// 4. Let extensible be ? IsExtensible(globalObject).
let extensible = global_object.is_extensible(context)?;
// 5. If hasProperty is false and extensible is true, then
if !has_property && extensible {
// a. Perform ? ObjRec.CreateMutableBinding(N, D).
self.object_record
.create_mutable_binding(name, deletion, false, context)?;
// b. Perform ? ObjRec.InitializeBinding(N, undefined).
self.object_record
.initialize_binding(name, JsValue::undefined(), context)?;
}
// 6. Let varDeclaredNames be envRec.[[VarNames]].
let mut var_declared_names = self.var_names.borrow_mut();
// 7. If varDeclaredNames does not contain N, then
if !var_declared_names.contains(&name) {
// a. Append N to varDeclaredNames.
var_declared_names.insert(name);
}
// 8. Return NormalCompletion(empty).
Ok(())
}
/// `9.1.1.4.18 CreateGlobalFunctionBinding ( N, V, D )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createglobalfunctionbinding
pub fn create_global_function_binding(
&mut self,
name: Sym,
value: JsValue,
deletion: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let ObjRec be envRec.[[ObjectRecord]].
// 2. Let globalObject be ObjRec.[[BindingObject]].
let global_object = &self.object_record.bindings;
// 3. Let existingProp be ? globalObject.[[GetOwnProperty]](N).
let existing_prop = global_object
.__get_own_property__(&context.interner().resolve_expect(name).into(), context)?;
// 4. If existingProp is undefined or existingProp.[[Configurable]] is true, then
let desc = if existing_prop.map_or(true, |f| f.expect_configurable()) {
// a. Let desc be the PropertyDescriptor { [[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: D }.
PropertyDescriptor::builder()
.value(value.clone())
.writable(true)
.enumerable(true)
.configurable(deletion)
.build()
// 5. Else,
} else {
// a. Let desc be the PropertyDescriptor { [[Value]]: V }.
PropertyDescriptor::builder().value(value.clone()).build()
};
let name_str = context.interner().resolve_expect(name).to_owned();
// 6. Perform ? DefinePropertyOrThrow(globalObject, N, desc).
global_object.define_property_or_throw(name_str.as_str(), desc, context)?;
// 7. Perform ? Set(globalObject, N, V, false).
global_object.set(name_str, value, false, context)?;
// 8. Let varDeclaredNames be envRec.[[VarNames]].
// 9. If varDeclaredNames does not contain N, then
if !self.var_names.borrow().contains(&name) {
// a. Append N to varDeclaredNames.
self.var_names.borrow_mut().insert(name);
}
// 10. Return NormalCompletion(empty).
Ok(())
}
}
impl EnvironmentRecordTrait for GlobalEnvironmentRecord {
/// `9.1.1.4.1 HasBinding ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-hasbinding-n
fn has_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, return true.
if self.declarative_record.has_binding(name, context)? {
return Ok(true);
}
// 3. Let ObjRec be envRec.[[ObjectRecord]].
// 4. Return ? ObjRec.HasBinding(N).
self.object_record.has_binding(name, context)
}
/// `9.1.1.4.2 CreateMutableBinding ( N, D )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-createmutablebinding-n-d
fn create_mutable_binding(
&self,
name: Sym,
deletion: bool,
allow_name_reuse: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, throw a TypeError exception.
if !allow_name_reuse && self.declarative_record.has_binding(name, context)? {
return context.throw_type_error(format!(
"Binding already exists for {}",
context.interner().resolve_expect(name)
));
}
// 3. Return DclRec.CreateMutableBinding(N, D).
self.declarative_record
.create_mutable_binding(name, deletion, allow_name_reuse, context)
}
/// `9.1.1.4.3 CreateImmutableBinding ( N, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-createimmutablebinding-n-s
fn create_immutable_binding(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, throw a TypeError exception.
if self.declarative_record.has_binding(name, context)? {
return context.throw_type_error(format!(
"Binding already exists for {}",
context.interner().resolve_expect(name)
));
}
// 3. Return DclRec.CreateImmutableBinding(N, S).
self.declarative_record
.create_immutable_binding(name, strict, context)
}
/// `9.1.1.4.4 InitializeBinding ( N, V )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-initializebinding-n-v
fn initialize_binding(&self, name: Sym, value: JsValue, context: &mut Context) -> JsResult<()> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, then
if self.declarative_record.has_binding(name, context)? {
// a. Return DclRec.InitializeBinding(N, V).
return self
.declarative_record
.initialize_binding(name, value, context);
}
// 3. Assert: If the binding exists, it must be in the object Environment Record.
assert!(
self.object_record.has_binding(name, context)?,
"Binding must be in object_record"
);
// 4. Let ObjRec be envRec.[[ObjectRecord]].
// 5. Return ? ObjRec.InitializeBinding(N, V).
self.object_record.initialize_binding(name, value, context)
}
/// `9.1.1.4.5 SetMutableBinding ( N, V, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-setmutablebinding-n-v-s
fn set_mutable_binding(
&self,
name: Sym,
value: JsValue,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, then
if self.declarative_record.has_binding(name, context)? {
// a. Return DclRec.SetMutableBinding(N, V, S).
return self
.declarative_record
.set_mutable_binding(name, value, strict, context);
}
// 3. Let ObjRec be envRec.[[ObjectRecord]].
// 4. Return ? ObjRec.SetMutableBinding(N, V, S).
self.object_record
.set_mutable_binding(name, value, strict, context)
}
/// `9.1.1.4.6 GetBindingValue ( N, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-getbindingvalue-n-s
fn get_binding_value(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, then
if self.declarative_record.has_binding(name, context)? {
// a. Return DclRec.GetBindingValue(N, S).
return self
.declarative_record
.get_binding_value(name, strict, context);
}
// 3. Let ObjRec be envRec.[[ObjectRecord]].
// 4. Return ? ObjRec.GetBindingValue(N, S).
self.object_record.get_binding_value(name, strict, context)
}
/// `9.1.1.4.7 DeleteBinding ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-deletebinding-n
fn delete_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let DclRec be envRec.[[DeclarativeRecord]].
// 2. If DclRec.HasBinding(N) is true, then
if self.declarative_record.has_binding(name, context)? {
// a. Return DclRec.DeleteBinding(N).
return self.declarative_record.delete_binding(name, context);
}
// 3. Let ObjRec be envRec.[[ObjectRecord]].
// 4. Let globalObject be ObjRec.[[BindingObject]].
let global_object = &self.object_record.bindings;
// 5. Let existingProp be ? HasOwnProperty(globalObject, N).
// 6. If existingProp is true, then
if global_object
.has_own_property(context.interner().resolve_expect(name).to_owned(), context)?
{
// a. Let status be ? ObjRec.DeleteBinding(N).
let status = self.object_record.delete_binding(name, context)?;
// b. If status is true, then
if status {
// i. Let varNames be envRec.[[VarNames]].
// ii. If N is an element of varNames, remove that element from the varNames.
self.var_names.borrow_mut().remove(&name);
}
// c. Return status.
return Ok(status);
}
// 7. Return true.
Ok(true)
}
/// `9.1.1.4.8 HasThisBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-hasthisbinding
fn has_this_binding(&self) -> bool {
// 1. Return true.
true
}
/// `9.1.1.4.9 HasSuperBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-hassuperbinding
fn has_super_binding(&self) -> bool {
// 1. Return false.
false
}
/// `9.1.1.4.10 WithBaseObject ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-withbaseobject
fn with_base_object(&self) -> Option<JsObject> {
// 1. Return undefined.
None
}
/// `9.1.1.4.11 GetThisBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-environment-records-getthisbinding
fn get_this_binding(&self, _context: &mut Context) -> JsResult<JsValue> {
// 1. Return envRec.[[GlobalThisValue]].
Ok(self.global_this_binding.clone().into())
}
fn get_outer_environment(&self) -> Option<Environment> {
None
}
fn get_outer_environment_ref(&self) -> Option<&Environment> {
None
}
fn set_outer_environment(&mut self, _env: Environment) {
// TODO: Implement
todo!("Not implemented yet")
}
fn get_environment_type(&self) -> EnvironmentType {
EnvironmentType::Global
}
fn recursive_create_mutable_binding(
&self,
name: Sym,
deletion: bool,
_scope: VariableScope,
context: &mut Context,
) -> JsResult<()> {
self.create_mutable_binding(name, deletion, false, context)
}
fn recursive_create_immutable_binding(
&self,
name: Sym,
deletion: bool,
_scope: VariableScope,
context: &mut Context,
) -> JsResult<()> {
self.create_immutable_binding(name, deletion, context)
}
fn recursive_set_mutable_binding(
&self,
name: Sym,
value: JsValue,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
self.set_mutable_binding(name, value, strict, context)
}
fn recursive_initialize_binding(
&self,
name: Sym,
value: JsValue,
context: &mut Context,
) -> JsResult<()> {
self.initialize_binding(name, value, context)
}
}
impl From<GlobalEnvironmentRecord> for Environment {
fn from(env: GlobalEnvironmentRecord) -> Self {
Gc::new(Box::new(env))
}
}

259
boa/src/environment/lexical_environment.rs

@ -1,259 +0,0 @@
//! # Lexical Environment
//!
//! <https://tc39.es/ecma262/#sec-lexical-environment-operations>
//!
//! The following operations are used to operate upon lexical environments
//! This is the entrypoint to lexical environments.
use super::global_environment_record::GlobalEnvironmentRecord;
use crate::{
environment::environment_record_trait::EnvironmentRecordTrait, gc::Gc, object::JsObject,
BoaProfiler, Context, JsResult, JsValue,
};
use boa_interner::Sym;
use std::{collections::VecDeque, error, fmt};
/// Environments are wrapped in a Box and then in a GC wrapper
pub type Environment = Gc<Box<dyn EnvironmentRecordTrait>>;
/// Give each environment an easy way to declare its own type
/// This helps with comparisons
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EnvironmentType {
Declarative,
Function,
Global,
Object,
}
/// The scope of a given variable
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VariableScope {
/// The variable declaration is scoped to the current block (`let` and `const`)
Block,
/// The variable declaration is scoped to the current function (`var`)
Function,
}
#[derive(Debug, Clone)]
pub struct LexicalEnvironment {
environment_stack: VecDeque<Environment>,
}
/// An error that occurred during lexing or compiling of the source input.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EnvironmentError {
details: String,
}
impl EnvironmentError {
pub fn new(msg: &str) -> Self {
Self {
details: msg.to_string(),
}
}
}
impl fmt::Display for EnvironmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl error::Error for EnvironmentError {}
impl LexicalEnvironment {
pub fn new(global: JsObject) -> Self {
let _timer = BoaProfiler::global().start_event("LexicalEnvironment::new", "env");
let global_env = GlobalEnvironmentRecord::new(global.clone(), global);
let mut lexical_env = Self {
environment_stack: VecDeque::new(),
};
// lexical_env.push(global_env);
lexical_env.environment_stack.push_back(global_env.into());
lexical_env
}
}
impl Context {
pub(crate) fn push_environment<T: Into<Environment>>(&mut self, env: T) {
self.realm
.environment
.environment_stack
.push_back(env.into());
}
pub(crate) fn pop_environment(&mut self) -> Option<Environment> {
self.realm.environment.environment_stack.pop_back()
}
pub(crate) fn get_this_binding(&mut self) -> JsResult<JsValue> {
self.get_current_environment()
.recursive_get_this_binding(self)
}
pub(crate) fn get_global_this_binding(&mut self) -> JsResult<JsValue> {
let global = self.realm.global_env.clone();
global.get_this_binding(self)
}
pub(crate) fn create_mutable_binding(
&mut self,
name: Sym,
deletion: bool,
scope: VariableScope,
) -> JsResult<()> {
self.get_current_environment()
.recursive_create_mutable_binding(name, deletion, scope, self)
}
pub(crate) fn create_immutable_binding(
&mut self,
name: Sym,
deletion: bool,
scope: VariableScope,
) -> JsResult<()> {
self.get_current_environment()
.recursive_create_immutable_binding(name, deletion, scope, self)
}
pub(crate) fn set_mutable_binding(
&mut self,
name: Sym,
value: JsValue,
strict: bool,
) -> JsResult<()> {
self.get_current_environment()
.recursive_set_mutable_binding(name, value, strict, self)
}
pub(crate) fn initialize_binding(&mut self, name: Sym, value: JsValue) -> JsResult<()> {
let _timer =
BoaProfiler::global().start_event("LexicalEnvironment::initialize_binding", "env");
self.get_current_environment()
.recursive_initialize_binding(name, value, self)
}
/// When neededing to clone an environment (linking it with another environnment)
/// cloning is more suited. The GC will remove the env once nothing is linking to it anymore
pub(crate) fn get_current_environment(&mut self) -> Environment {
let _timer =
BoaProfiler::global().start_event("LexicalEnvironment::get_current_environment", "env");
self.realm
.environment
.environment_stack
.back_mut()
.expect("Could not get mutable reference to back object")
.clone()
}
pub(crate) fn has_binding(&mut self, name: Sym) -> JsResult<bool> {
let _timer = BoaProfiler::global().start_event("LexicalEnvironment::has_binding", "env");
self.get_current_environment()
.recursive_has_binding(name, self)
}
pub(crate) fn get_binding_value(&mut self, name: Sym) -> JsResult<JsValue> {
let _timer =
BoaProfiler::global().start_event("LexicalEnvironment::get_binding_value", "env");
self.get_current_environment()
.recursive_get_binding_value(name, self)
}
}
#[cfg(test)]
mod tests {
use crate::exec;
#[test]
fn let_is_blockscoped() {
let scenario = r#"
{
let bar = "bar";
}
try{
bar;
} catch (err) {
err.message
}
"#;
assert_eq!(&exec(scenario), "\"bar is not defined\"");
}
#[test]
fn const_is_blockscoped() {
let scenario = r#"
{
const bar = "bar";
}
try{
bar;
} catch (err) {
err.message
}
"#;
assert_eq!(&exec(scenario), "\"bar is not defined\"");
}
#[test]
fn var_not_blockscoped() {
let scenario = r#"
{
var bar = "bar";
}
bar == "bar";
"#;
assert_eq!(&exec(scenario), "true");
}
#[test]
fn functions_use_declaration_scope() {
let scenario = r#"
function foo() {
try {
bar;
} catch (err) {
return err.message;
}
}
{
let bar = "bar";
foo();
}
"#;
assert_eq!(&exec(scenario), "\"bar is not defined\"");
}
#[test]
fn set_outer_var_in_blockscope() {
let scenario = r#"
var bar;
{
bar = "foo";
}
bar == "foo";
"#;
assert_eq!(&exec(scenario), "true");
}
#[test]
fn set_outer_let_in_blockscope() {
let scenario = r#"
let bar;
{
bar = "foo";
}
bar == "foo";
"#;
assert_eq!(&exec(scenario), "true");
}
}

8
boa/src/environment/mod.rs

@ -1,8 +0,0 @@
//! Environment handling, lexical, object, function and declaritive records
pub mod declarative_environment_record;
pub mod environment_record_trait;
pub mod function_environment_record;
pub mod global_environment_record;
pub mod lexical_environment;
pub mod object_environment_record;

281
boa/src/environment/object_environment_record.rs

@ -1,281 +0,0 @@
//! # Object Records
//!
//! Each object Environment Record is associated with an object called its binding object.
//! An object Environment Record binds the set of string identifier names that directly
//! correspond to the property names of its binding object.
//! Property keys that are not strings in the form of an `IdentifierName` are not included in the set of bound identifiers.
//! More info: [Object Records](https://tc39.es/ecma262/#sec-object-environment-records)
use crate::{
environment::{
environment_record_trait::EnvironmentRecordTrait,
lexical_environment::{Environment, EnvironmentType},
},
gc::{Finalize, Gc, Trace},
object::JsObject,
property::PropertyDescriptor,
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_interner::Sym;
#[derive(Debug, Trace, Finalize, Clone)]
pub struct ObjectEnvironmentRecord {
pub bindings: JsObject,
pub with_environment: bool,
pub outer_env: Option<Environment>,
}
impl ObjectEnvironmentRecord {
pub fn new(object: JsObject, environment: Option<Environment>) -> Self {
Self {
bindings: object,
outer_env: environment,
/// Object Environment Records created for with statements (13.11)
/// can provide their binding object as an implicit this value for use in function calls.
/// The capability is controlled by a withEnvironment Boolean value that is associated
/// with each object Environment Record. By default, the value of withEnvironment is false
/// for any object Environment Record.
with_environment: false,
}
}
}
impl EnvironmentRecordTrait for ObjectEnvironmentRecord {
/// `9.1.1.2.1 HasBinding ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-hasbinding-n
fn has_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let bindingObject be envRec.[[BindingObject]].
// 2. Let foundBinding be ? HasProperty(bindingObject, N).
// 3. If foundBinding is false, return false.
if !self
.bindings
.has_property(context.interner().resolve_expect(name).to_owned(), context)?
{
return Ok(false);
}
// 4. If envRec.[[IsWithEnvironment]] is false, return true.
if !self.with_environment {
return Ok(true);
}
// 5. Let unscopables be ? Get(bindingObject, @@unscopables).
// 6. If Type(unscopables) is Object, then
if let Some(unscopables) = self
.bindings
.get(WellKnownSymbols::unscopables(), context)?
.as_object()
{
// a. Let blocked be ! ToBoolean(? Get(unscopables, N)).
// b. If blocked is true, return false.
if unscopables
.get(context.interner().resolve_expect(name).to_owned(), context)?
.to_boolean()
{
return Ok(false);
}
}
// 7. Return true.
Ok(true)
}
/// `9.1.1.2.2 CreateMutableBinding ( N, D )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-createmutablebinding-n-d
fn create_mutable_binding(
&self,
name: Sym,
deletion: bool,
_allow_name_reuse: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let bindingObject be envRec.[[BindingObject]].
// 2. Return ? DefinePropertyOrThrow(bindingObject, N, PropertyDescriptor { [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: D }).
self.bindings.define_property_or_throw(
context.interner().resolve_expect(name).to_owned(),
PropertyDescriptor::builder()
.value(JsValue::undefined())
.writable(true)
.enumerable(true)
.configurable(deletion),
context,
)?;
Ok(())
}
/// `9.1.1.2.3 CreateImmutableBinding ( N, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-createimmutablebinding-n-s
fn create_immutable_binding(
&self,
_name: Sym,
_strict: bool,
_context: &mut Context,
) -> JsResult<()> {
Ok(())
}
/// `9.1.1.2.4 InitializeBinding ( N, V )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-initializebinding-n-v
fn initialize_binding(&self, name: Sym, value: JsValue, context: &mut Context) -> JsResult<()> {
// 1. Return ? envRec.SetMutableBinding(N, V, false).
self.set_mutable_binding(name, value, false, context)
}
/// `9.1.1.2.5 SetMutableBinding ( N, V, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-setmutablebinding-n-v-s
fn set_mutable_binding(
&self,
name: Sym,
value: JsValue,
strict: bool,
context: &mut Context,
) -> JsResult<()> {
// 1. Let bindingObject be envRec.[[BindingObject]].
// 2. Let stillExists be ? HasProperty(bindingObject, N).
let still_exists = self
.bindings
.has_property(context.interner().resolve_expect(name).to_owned(), context)?;
// 3. If stillExists is false and S is true, throw a ReferenceError exception.
if !still_exists && strict {
return context.throw_reference_error("Binding already exists");
}
// 4. Return ? Set(bindingObject, N, V, S).
self.bindings.set(
context.interner().resolve_expect(name).to_owned(),
value,
strict,
context,
)?;
Ok(())
}
/// `9.1.1.2.6 GetBindingValue ( N, S )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-getbindingvalue-n-s
fn get_binding_value(
&self,
name: Sym,
strict: bool,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let bindingObject be envRec.[[BindingObject]].
// 2. Let value be ? HasProperty(bindingObject, N).
// 3. If value is false, then
if !self
.bindings
.__has_property__(&context.interner().resolve_expect(name).into(), context)?
{
// a. If S is false, return the value undefined; otherwise throw a ReferenceError exception.
if !strict {
return Ok(JsValue::undefined());
}
return context.throw_reference_error(format!(
"{} has no binding",
context.interner().resolve_expect(name)
));
}
// 4. Return ? Get(bindingObject, N).
self.bindings
.get(context.interner().resolve_expect(name).to_owned(), context)
}
/// `9.1.1.2.7 DeleteBinding ( N )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-deletebinding-n
fn delete_binding(&self, name: Sym, context: &mut Context) -> JsResult<bool> {
// 1. Let bindingObject be envRec.[[BindingObject]].
// 2. Return ? bindingObject.[[Delete]](N).
self.bindings
.__delete__(&context.interner().resolve_expect(name).into(), context)
}
/// `9.1.1.2.8 HasThisBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-hasthisbinding
fn has_this_binding(&self) -> bool {
// 1. Return false.
false
}
/// `9.1.1.2.9 HasSuperBinding ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-hassuperbinding
fn has_super_binding(&self) -> bool {
// 1. Return false.
false
}
/// `9.1.1.2.10 WithBaseObject ( )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-object-environment-records-hassuperbinding
fn with_base_object(&self) -> Option<JsObject> {
// 1. If envRec.[[IsWithEnvironment]] is true, return envRec.[[BindingObject]].
// 2. Otherwise, return undefined.
if self.with_environment {
Some(self.bindings.clone())
} else {
None
}
}
fn get_this_binding(&self, _context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn get_outer_environment_ref(&self) -> Option<&Environment> {
self.outer_env.as_ref()
}
fn set_outer_environment(&mut self, env: Environment) {
self.outer_env = Some(env);
}
fn get_environment_type(&self) -> EnvironmentType {
EnvironmentType::Function
}
}
impl From<ObjectEnvironmentRecord> for Environment {
fn from(env: ObjectEnvironmentRecord) -> Self {
Gc::new(Box::new(env))
}
}

315
boa/src/environments/compile.rs

@ -0,0 +1,315 @@
use crate::{
environments::runtime::BindingLocator, property::PropertyDescriptor, Context, JsResult,
JsString, JsValue,
};
use boa_interner::Sym;
use rustc_hash::FxHashMap;
/// A compile time binding represents a binding at bytecode compile time in a [`CompileTimeEnvironment`].
///
/// It contains the binding index and a flag to indicate if this is a mutable binding or not.
#[derive(Debug)]
struct CompileTimeBinding {
index: usize,
mutable: bool,
}
/// A compile time environment maps bound identifiers to their binding positions.
///
/// A compile time environment also indicates, if it is a function environment.
#[derive(Debug)]
pub(crate) struct CompileTimeEnvironment {
bindings: FxHashMap<Sym, CompileTimeBinding>,
function_scope: bool,
}
impl CompileTimeEnvironment {
/// Returns the number of bindings in this environment.
#[inline]
pub(crate) fn num_bindings(&self) -> usize {
self.bindings.len()
}
}
/// The compile time environment stack contains a stack of all environments at bytecode compile time.
///
/// The first environment on the stack represents the global environment.
/// This is never being deleted and is tied to the existence of the realm.
/// All other environments are being dropped once they are not needed anymore.
#[derive(Debug)]
pub(crate) struct CompileTimeEnvironmentStack {
stack: Vec<CompileTimeEnvironment>,
}
impl CompileTimeEnvironmentStack {
/// Creates a new compile time environment stack.
///
/// This function should only be used once, on realm creation.
#[inline]
pub(crate) fn new() -> Self {
Self {
stack: vec![CompileTimeEnvironment {
bindings: FxHashMap::default(),
function_scope: true,
}],
}
}
/// Get the number of bindings for the current last environment.
///
/// # Panics
///
/// Panics if there are no environments on the stack.
#[inline]
pub(crate) fn get_binding_number(&self) -> usize {
self.stack
.last()
.expect("global environment must always exist")
.num_bindings()
}
}
impl Context {
/// Push either a new declarative or function environment on the compile time environment stack.
///
/// Note: This function only works at bytecode compile time!
#[inline]
pub(crate) fn push_compile_time_environment(&mut self, function_scope: bool) {
self.realm.compile_env.stack.push(CompileTimeEnvironment {
bindings: FxHashMap::default(),
function_scope,
});
}
/// Pop the last compile time environment from the stack.
///
/// Note: This function only works at bytecode compile time!
///
/// # Panics
///
/// Panics if there are no more environments that can be pop'ed.
#[inline]
pub(crate) fn pop_compile_time_environment(&mut self) -> CompileTimeEnvironment {
assert!(
self.realm.compile_env.stack.len() > 1,
"cannot pop global environment"
);
self.realm
.compile_env
.stack
.pop()
.expect("len > 1 already checked")
}
/// Get the number of bindings for the current compile time environment.
///
/// Note: This function only works at bytecode compile time!
///
/// # Panics
///
/// Panics if there are no environments on the compile time environment stack.
#[inline]
pub(crate) fn get_binding_number(&self) -> usize {
self.realm
.compile_env
.stack
.last()
.expect("global environment must always exist")
.num_bindings()
}
/// Get the binding locator of the binding at bytecode compile time.
///
/// Note: This function only works at bytecode compile time!
#[inline]
pub(crate) fn get_binding_value(&self, name: Sym) -> BindingLocator {
for (i, env) in self.realm.compile_env.stack.iter().enumerate().rev() {
if let Some(binding) = env.bindings.get(&name) {
return BindingLocator::declarative(name, i, binding.index);
}
}
BindingLocator::global(name)
}
/// Return if a declarative binding exists at bytecode compile time.
/// This does not include bindings on the global object.
///
/// Note: This function only works at bytecode compile time!
#[inline]
pub(crate) fn has_binding(&self, name: Sym) -> bool {
for env in self.realm.compile_env.stack.iter().rev() {
if env.bindings.contains_key(&name) {
return true;
}
}
false
}
/// Create a mutable binding at bytecode compile time.
/// This function returns a syntax error, if the binding is a redeclaration.
///
/// Note: This function only works at bytecode compile time!
///
/// # Panics
///
/// Panics if the global environment is not function scoped.
#[inline]
pub(crate) fn create_mutable_binding(
&mut self,
name: Sym,
function_scope: bool,
allow_name_reuse: bool,
) -> JsResult<()> {
let name_str = JsString::from(self.interner().resolve_expect(name));
for (i, env) in self.realm.compile_env.stack.iter_mut().enumerate().rev() {
if !function_scope || env.function_scope {
if env.bindings.contains_key(&name) {
if allow_name_reuse {
return Ok(());
}
return self
.throw_syntax_error(format!("Redeclaration of variable {}", name_str));
}
if i == 0 {
let desc = self
.realm
.global_property_map
.string_property_map()
.get(&name_str);
let non_configurable_binding_exists = match desc {
Some(desc) => !matches!(desc.configurable(), Some(true)),
None => false,
};
if function_scope && desc.is_none() {
self.global_bindings_mut().insert(
name_str,
PropertyDescriptor::builder()
.value(JsValue::Undefined)
.writable(true)
.enumerable(true)
.configurable(true)
.build(),
);
return Ok(());
} else if function_scope {
return Ok(());
} else if !function_scope
&& !allow_name_reuse
&& non_configurable_binding_exists
{
return self
.throw_syntax_error(format!("Redeclaration of variable {}", name_str));
}
}
let binding_index = env.bindings.len();
env.bindings.insert(
name,
CompileTimeBinding {
index: binding_index,
mutable: true,
},
);
return Ok(());
}
continue;
}
panic!("global environment must be function scoped")
}
/// Initialize a mutable binding at bytecode compile time and return it's binding locator.
///
/// Note: This function only works at bytecode compile time!
#[inline]
pub(crate) fn initialize_mutable_binding(
&self,
name: Sym,
function_scope: bool,
) -> BindingLocator {
for (i, env) in self.realm.compile_env.stack.iter().enumerate().rev() {
if function_scope && !env.function_scope {
continue;
}
if let Some(binding) = env.bindings.get(&name) {
return BindingLocator::declarative(name, i, binding.index);
}
return BindingLocator::global(name);
}
BindingLocator::global(name)
}
/// Create an immutable binding at bytecode compile time.
/// This function returns a syntax error, if the binding is a redeclaration.
///
/// Note: This function only works at bytecode compile time!
///
/// # Panics
///
/// Panics if the global environment does not exist.
#[inline]
pub(crate) fn create_immutable_binding(&mut self, name: Sym) -> JsResult<()> {
let name_str = JsString::from(self.interner().resolve_expect(name));
let exists_global = self.realm.compile_env.stack.len() == 1
&& self.global_bindings().contains_key(&name_str);
let env = self
.realm
.compile_env
.stack
.last_mut()
.expect("global environment must always exist");
if env.bindings.contains_key(&name) || exists_global {
self.throw_syntax_error(format!("Redeclaration of variable {}", name_str))
} else {
let binding_index = env.bindings.len();
env.bindings.insert(
name,
CompileTimeBinding {
index: binding_index,
mutable: false,
},
);
Ok(())
}
}
/// Initialize an immutable binding at bytecode compile time and return it's binding locator.
///
/// Note: This function only works at bytecode compile time!
///
/// # Panics
///
/// Panics if the global environment does not exist or a the binding was not created on the current environment.
#[inline]
pub(crate) fn initialize_immutable_binding(&self, name: Sym) -> BindingLocator {
let environment_index = self.realm.compile_env.stack.len() - 1;
let env = self
.realm
.compile_env
.stack
.last()
.expect("global environment must always exist");
let binding = env.bindings.get(&name).expect("binding must exist");
BindingLocator::declarative(name, environment_index, binding.index)
}
/// Return the binding locator for a set operation on an existing binding.
///
/// Note: This function only works at bytecode compile time!
#[inline]
pub(crate) fn set_mutable_binding(&self, name: Sym) -> BindingLocator {
for (i, env) in self.realm.compile_env.stack.iter().enumerate().rev() {
if let Some(binding) = env.bindings.get(&name) {
if binding.mutable {
return BindingLocator::declarative(name, i, binding.index);
}
return BindingLocator::mutate_immutable(name);
}
}
BindingLocator::global(name)
}
}

36
boa/src/environments/mod.rs

@ -0,0 +1,36 @@
//! This module implements ECMAScript `Environment Records`.
//!
//! Environments contain the bindings of identifiers to their values.
//! The implementation differs from the methods defined by the specification,
//! but the resulting behavior should be the same.
//!
//! To make the runtime more performant, environment specific behavior is split
//! between bytecode compilation and the runtime.
//! While the association of identifiers to values seems like a natural fit for a hashmap,
//! lookups of the values at runtime are very expensive.
//! Environments can also have outer environments.
//! In the worst case, there are as many hashmap lookups, as there are environments.
//!
//! To avoid these costs, hashmaps are not used at runtime.
//! At runtime, environments are represented as fixed size lists of binding values.
//! The positions of the bindings in these lists is determined at bytecode compile time.
//!
//! A binding is uniquely identified by two indices:
//! - An environment index, that identifies the environment in which the binding exists
//! - A binding index, that identifies the binding in the environment
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-environment-records
mod compile;
mod runtime;
pub(crate) use {
compile::CompileTimeEnvironmentStack,
runtime::{BindingLocator, DeclarativeEnvironment, DeclarativeEnvironmentStack},
};
#[cfg(test)]
mod tests;

332
boa/src/environments/runtime.rs

@ -0,0 +1,332 @@
use crate::{
gc::{Finalize, Gc, Trace},
Context, JsResult, JsValue,
};
use boa_interner::Sym;
use gc::GcCell;
/// A declarative environment holds the bindings values at runtime.
///
/// Bindings are stored in a fixed size list of optional values.
/// If a binding is not initialized, the value is `None`.
///
/// Optionally, an environment can hold a `this` value.
/// The `this` value is present only if the environment is a function environment.
#[derive(Debug, Trace, Finalize)]
pub(crate) struct DeclarativeEnvironment {
bindings: GcCell<Vec<Option<JsValue>>>,
this: Option<JsValue>,
}
impl DeclarativeEnvironment {
/// Get the binding value from the environment by it's index.
///
/// # Panics
///
/// Panics if the binding value is out of range or not initialized.
#[inline]
pub(crate) fn get(&self, index: usize) -> JsValue {
self.bindings
.borrow()
.get(index)
.expect("binding index must be in range")
.clone()
.expect("binding must be initialized")
}
/// Set the binding value at the specified index.
///
/// # Panics
///
/// Panics if the binding value is out of range or not initialized.
#[inline]
pub(crate) fn set(&self, index: usize, value: JsValue) {
let mut bindings = self.bindings.borrow_mut();
let binding = bindings
.get_mut(index)
.expect("binding index must be in range");
assert!(!binding.is_none(), "binding must be initialized");
*binding = Some(value);
}
}
/// A declarative environment stack holds all declarative environments at runtime.
///
/// Environments themselves are garbage collected,
/// because they must be preserved for function calls.
#[derive(Clone, Debug, Trace, Finalize)]
pub struct DeclarativeEnvironmentStack {
stack: Vec<Gc<DeclarativeEnvironment>>,
}
impl DeclarativeEnvironmentStack {
/// Create a new environment stack with the most outer declarative environment.
#[inline]
pub(crate) fn new() -> Self {
Self {
stack: vec![Gc::new(DeclarativeEnvironment {
bindings: GcCell::new(Vec::new()),
this: None,
})],
}
}
/// Set the number of bindings on the global environment.
///
/// # Panics
///
/// Panics if no environment exists on the stack.
#[inline]
pub(crate) fn set_global_binding_number(&mut self, binding_number: usize) {
let environment = self
.stack
.get(0)
.expect("global environment must always exist");
let mut bindings = environment.bindings.borrow_mut();
if bindings.len() < binding_number {
bindings.resize(binding_number, None);
}
}
/// Get the `this` value of the most outer function environment.
#[inline]
pub(crate) fn get_last_this(&self) -> Option<JsValue> {
for env in self.stack.iter().rev() {
if let Some(this) = &env.this {
return Some(this.clone());
}
}
None
}
/// Push a declarative environment on the environments stack.
#[inline]
pub(crate) fn push_declarative(&mut self, num_bindings: usize) {
self.stack.push(Gc::new(DeclarativeEnvironment {
bindings: GcCell::new(vec![None; num_bindings]),
this: None,
}));
}
/// Push a function environment on the environments stack.
#[inline]
pub(crate) fn push_function(&mut self, num_bindings: usize, this: JsValue) {
self.stack.push(Gc::new(DeclarativeEnvironment {
bindings: GcCell::new(vec![None; num_bindings]),
this: Some(this),
}));
}
/// Pop environment from the environments stack.
#[inline]
pub(crate) fn pop(&mut self) {
debug_assert!(self.stack.len() > 1);
self.stack.pop();
}
/// Get the most outer environment.
///
/// # Panics
///
/// Panics if no environment exists on the stack.
#[inline]
pub(crate) fn current(&mut self) -> Gc<DeclarativeEnvironment> {
self.stack
.last()
.expect("global environment must always exist")
.clone()
}
/// Get the value of a binding.
///
/// # Panics
///
/// Panics if the environment or binding index are out of range.
#[inline]
pub(crate) fn get_value_optional(
&self,
environment_index: usize,
binding_index: usize,
) -> Option<JsValue> {
self.stack
.get(environment_index)
.expect("environment index must be in range")
.bindings
.borrow()
.get(binding_index)
.expect("binding index must be in range")
.clone()
}
/// Set the value of a binding.
///
/// # Panics
///
/// Panics if the environment or binding index are out of range.
#[inline]
pub(crate) fn put_value(
&mut self,
environment_index: usize,
binding_index: usize,
value: JsValue,
) {
let mut bindings = self
.stack
.get(environment_index)
.expect("environment index must be in range")
.bindings
.borrow_mut();
let binding = bindings
.get_mut(binding_index)
.expect("binding index must be in range");
*binding = Some(value);
}
/// Set the value of a binding if it is initialized.
/// Return `true` if the value has been set.
///
/// # Panics
///
/// Panics if the environment or binding index are out of range.
#[inline]
pub(crate) fn put_value_if_initialized(
&mut self,
environment_index: usize,
binding_index: usize,
value: JsValue,
) -> bool {
let mut bindings = self
.stack
.get(environment_index)
.expect("environment index must be in range")
.bindings
.borrow_mut();
let binding = bindings
.get_mut(binding_index)
.expect("binding index must be in range");
if binding.is_none() {
false
} else {
*binding = Some(value);
true
}
}
/// Set the value of a binding if it is uninitialized.
///
/// # Panics
///
/// Panics if the environment or binding index are out of range.
#[inline]
pub(crate) fn put_value_if_uninitialized(
&mut self,
environment_index: usize,
binding_index: usize,
value: JsValue,
) {
let mut bindings = self
.stack
.get(environment_index)
.expect("environment index must be in range")
.bindings
.borrow_mut();
let binding = bindings
.get_mut(binding_index)
.expect("binding index must be in range");
if binding.is_none() {
*binding = Some(value);
}
}
}
/// A binding locator contains all information about a binding that is needed to resolve it at runtime.
///
/// Binding locators get created at bytecode compile time and are accessible at runtime via the [`crate::vm::CodeBlock`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct BindingLocator {
name: Sym,
environment_index: usize,
binding_index: usize,
global: bool,
mutate_immutable: bool,
}
impl BindingLocator {
/// Creates a new declarative binding locator that has knows indices.
#[inline]
pub(in crate::environments) fn declarative(
name: Sym,
environment_index: usize,
binding_index: usize,
) -> Self {
Self {
name,
environment_index,
binding_index,
global: false,
mutate_immutable: false,
}
}
/// Creates a binding locator that indicates that the binding is on the global object.
#[inline]
pub(in crate::environments) fn global(name: Sym) -> Self {
Self {
name,
environment_index: 0,
binding_index: 0,
global: true,
mutate_immutable: false,
}
}
/// Creates a binding locator that indicates that it was attempted to mutate an immutable binding.
/// At runtime this should always produce a type error.
#[inline]
pub(in crate::environments) fn mutate_immutable(name: Sym) -> Self {
Self {
name,
environment_index: 0,
binding_index: 0,
global: false,
mutate_immutable: true,
}
}
/// Returns the name of the binding.
#[inline]
pub(crate) fn name(&self) -> Sym {
self.name
}
/// Returns if the binding is located on the global object.
#[inline]
pub(crate) fn is_global(&self) -> bool {
self.global
}
/// Returns the environment index of the binding.
#[inline]
pub(crate) fn environment_index(&self) -> usize {
self.environment_index
}
/// Returns the binding index of the binding.
#[inline]
pub(crate) fn binding_index(&self) -> usize {
self.binding_index
}
/// Helper method to throws an error if the binding access is illegal.
#[inline]
pub(crate) fn throw_mutate_immutable(&self, context: &mut Context) -> JsResult<()> {
if self.mutate_immutable {
context.throw_type_error(format!(
"cannot mutate an immutable binding '{}'",
context.interner().resolve_expect(self.name)
))
} else {
Ok(())
}
}
}

92
boa/src/environments/tests.rs

@ -0,0 +1,92 @@
use crate::exec;
#[test]
fn let_is_block_scoped() {
let scenario = r#"
{
let bar = "bar";
}
try{
bar;
} catch (err) {
err.message
}
"#;
assert_eq!(&exec(scenario), "\"bar is not defined\"");
}
#[test]
fn const_is_block_scoped() {
let scenario = r#"
{
const bar = "bar";
}
try{
bar;
} catch (err) {
err.message
}
"#;
assert_eq!(&exec(scenario), "\"bar is not defined\"");
}
#[test]
fn var_not_block_scoped() {
let scenario = r#"
{
var bar = "bar";
}
bar == "bar";
"#;
assert_eq!(&exec(scenario), "true");
}
#[test]
fn functions_use_declaration_scope() {
let scenario = r#"
function foo() {
try {
bar;
} catch (err) {
return err.message;
}
}
{
let bar = "bar";
foo();
}
"#;
assert_eq!(&exec(scenario), "\"bar is not defined\"");
}
#[test]
fn set_outer_var_in_block_scope() {
let scenario = r#"
var bar;
{
bar = "foo";
}
bar == "foo";
"#;
assert_eq!(&exec(scenario), "true");
}
#[test]
fn set_outer_let_in_block_scope() {
let scenario = r#"
let bar;
{
bar = "foo";
}
bar == "foo";
"#;
assert_eq!(&exec(scenario), "true");
}

2
boa/src/lib.rs

@ -72,7 +72,7 @@ pub mod builtins;
pub mod bytecompiler;
pub mod class;
pub mod context;
pub mod environment;
pub mod environments;
pub mod gc;
pub mod object;
pub mod profiler;

460
boa/src/object/internal_methods/global.rs

@ -0,0 +1,460 @@
use crate::{
object::{InternalObjectMethods, JsObject, ORDINARY_INTERNAL_METHODS},
property::{DescriptorKind, PropertyDescriptor, PropertyKey},
value::JsValue,
BoaProfiler, Context, JsResult,
};
/// Definitions of the internal object methods for global object.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-global-object
pub(crate) static GLOBAL_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods {
__get_own_property__: global_get_own_property,
__is_extensible__: global_is_extensible,
__prevent_extensions__: global_prevent_extensions,
__define_own_property__: global_define_own_property,
__has_property__: global_has_property,
__get__: global_get,
__set__: global_set,
__delete__: global_delete,
..ORDINARY_INTERNAL_METHODS
};
/// Abstract operation `OrdinaryGetOwnProperty`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinarygetownproperty
#[inline]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn global_get_own_property(
_obj: &JsObject,
key: &PropertyKey,
context: &mut Context,
) -> JsResult<Option<PropertyDescriptor>> {
let _timer = BoaProfiler::global().start_event("Object::global_get_own_property", "object");
// 1. Assert: IsPropertyKey(P) is true.
// 2. If O does not have an own property with key P, return undefined.
// 3. Let D be a newly created Property Descriptor with no fields.
// 4. Let X be O's own property whose key is P.
// 5. If X is a data property, then
// a. Set D.[[Value]] to the value of X's [[Value]] attribute.
// b. Set D.[[Writable]] to the value of X's [[Writable]] attribute.
// 6. Else,
// a. Assert: X is an accessor property.
// b. Set D.[[Get]] to the value of X's [[Get]] attribute.
// c. Set D.[[Set]] to the value of X's [[Set]] attribute.
// 7. Set D.[[Enumerable]] to the value of X's [[Enumerable]] attribute.
// 8. Set D.[[Configurable]] to the value of X's [[Configurable]] attribute.
// 9. Return D.
Ok(context.realm.global_property_map.get(key).cloned())
}
/// Abstract operation `OrdinaryIsExtensible`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinaryisextensible
#[inline]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn global_is_extensible(_obj: &JsObject, context: &mut Context) -> JsResult<bool> {
// 1. Return O.[[Extensible]].
Ok(context.realm.global_extensible)
}
/// Abstract operation `OrdinaryPreventExtensions`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinarypreventextensions
#[inline]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn global_prevent_extensions(_obj: &JsObject, context: &mut Context) -> JsResult<bool> {
// 1. Set O.[[Extensible]] to false.
context.realm.global_extensible = false;
// 2. Return true.
Ok(true)
}
/// Abstract operation `OrdinaryDefineOwnProperty`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinarydefineownproperty
#[inline]
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn global_define_own_property(
obj: &JsObject,
key: PropertyKey,
desc: PropertyDescriptor,
context: &mut Context,
) -> JsResult<bool> {
let _timer = BoaProfiler::global().start_event("Object::global_define_own_property", "object");
// 1. Let current be ? O.[[GetOwnProperty]](P).
let current = global_get_own_property(obj, &key, context)?;
// 2. Let extensible be ? IsExtensible(O).
let extensible = obj.__is_extensible__(context)?;
// 3. Return ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current).
Ok(validate_and_apply_property_descriptor(
&key, extensible, desc, current, context,
))
}
/// Abstract operation `OrdinaryHasProperty`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinaryhasproperty
#[inline]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn global_has_property(
_obj: &JsObject,
key: &PropertyKey,
context: &mut Context,
) -> JsResult<bool> {
let _timer = BoaProfiler::global().start_event("Object::global_has_property", "object");
Ok(context.realm.global_property_map.contains_key(key))
}
/// Abstract operation `OrdinaryGet`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinaryget
#[inline]
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn global_get(
obj: &JsObject,
key: &PropertyKey,
receiver: JsValue,
context: &mut Context,
) -> JsResult<JsValue> {
let _timer = BoaProfiler::global().start_event("Object::global_get", "object");
// 1. Assert: IsPropertyKey(P) is true.
// 2. Let desc be ? O.[[GetOwnProperty]](P).
match global_get_own_property(obj, key, context)? {
// If desc is undefined, then
None => {
// b. If parent is null, return undefined.
Ok(JsValue::undefined())
}
Some(ref desc) => match desc.kind() {
// 4. If IsDataDescriptor(desc) is true, return desc.[[Value]].
DescriptorKind::Data {
value: Some(value), ..
} => Ok(value.clone()),
// 5. Assert: IsAccessorDescriptor(desc) is true.
// 6. Let getter be desc.[[Get]].
DescriptorKind::Accessor { get: Some(get), .. } if !get.is_undefined() => {
// 8. Return ? Call(getter, Receiver).
context.call(get, &receiver, &[])
}
// 7. If getter is undefined, return undefined.
_ => Ok(JsValue::undefined()),
},
}
}
/// Abstract operation `OrdinarySet`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinaryset
#[inline]
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn global_set(
_obj: &JsObject,
key: PropertyKey,
value: JsValue,
_receiver: JsValue,
context: &mut Context,
) -> JsResult<bool> {
global_set_no_receiver(&key, value, context)
}
#[inline]
pub(crate) fn global_set_no_receiver(
key: &PropertyKey,
value: JsValue,
context: &mut Context,
) -> JsResult<bool> {
let _timer = BoaProfiler::global().start_event("Object::global_set", "object");
// 1. Assert: IsPropertyKey(P) is true.
// 2. Let ownDesc be ? O.[[GetOwnProperty]](P).
// 3. Return OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc).
// OrdinarySetWithOwnDescriptor ( O, P, V, Receiver, ownDesc )
// https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-ordinarysetwithowndescriptor
// 1. Assert: IsPropertyKey(P) is true.
let own_desc = if let Some(desc) = context.realm.global_property_map.get(key).cloned() {
desc
}
// c. Else,
else {
PropertyDescriptor::builder()
.value(value.clone())
.writable(true)
.enumerable(true)
.configurable(true)
.build()
};
// 3. If IsDataDescriptor(ownDesc) is true, then
if own_desc.is_data_descriptor() {
// a. If ownDesc.[[Writable]] is false, return false.
if !own_desc.expect_writable() {
return Ok(false);
}
// c. Let existingDescriptor be ? Receiver.[[GetOwnProperty]](P).
// d. If existingDescriptor is not undefined, then
let desc = if let Some(existing_desc) = context.realm.global_property_map.get(key) {
// i. If IsAccessorDescriptor(existingDescriptor) is true, return false.
if existing_desc.is_accessor_descriptor() {
return Ok(false);
}
// ii. If existingDescriptor.[[Writable]] is false, return false.
if !existing_desc.expect_writable() {
return Ok(false);
}
// iii. Let valueDesc be the PropertyDescriptor { [[Value]]: V }.
// iv. Return ? Receiver.[[DefineOwnProperty]](P, valueDesc).
PropertyDescriptor::builder().value(value).build()
} else {
// i. Assert: Receiver does not currently have a property P.
// ii. Return ? CreateDataProperty(Receiver, P, V).
PropertyDescriptor::builder()
.value(value)
.writable(true)
.enumerable(true)
.configurable(true)
.build()
};
// 1. Let current be ? O.[[GetOwnProperty]](P).
let current = context.realm.global_property_map.get(key).cloned();
// 2. Let extensible be ? IsExtensible(O).
let extensible = context.realm.global_extensible;
// 3. Return ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current).
return Ok(validate_and_apply_property_descriptor(
key, extensible, desc, current, context,
));
}
// 4. Assert: IsAccessorDescriptor(ownDesc) is true.
debug_assert!(own_desc.is_accessor_descriptor());
// 5. Let setter be ownDesc.[[Set]].
match own_desc.set() {
Some(set) if !set.is_undefined() => {
// 7. Perform ? Call(setter, Receiver, « V »).
context.call(set, &context.global_object().clone().into(), &[value])?;
// 8. Return true.
Ok(true)
}
// 6. If setter is undefined, return false.
_ => Ok(false),
}
}
/// Abstract operation `OrdinaryDelete`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ordinarydelete
#[inline]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn global_delete(
_obj: &JsObject,
key: &PropertyKey,
context: &mut Context,
) -> JsResult<bool> {
let _timer = BoaProfiler::global().start_event("Object::global_delete", "object");
// 1. Assert: IsPropertyKey(P) is true.
// 2. Let desc be ? O.[[GetOwnProperty]](P).
match context.realm.global_property_map.get(key) {
// 4. If desc.[[Configurable]] is true, then
Some(desc) if desc.expect_configurable() => {
// a. Remove the own property with name P from O.
context.realm.global_property_map.remove(key);
// b. Return true.
Ok(true)
}
// 5. Return false.
Some(_) => Ok(false),
// 3. If desc is undefined, return true.
None => Ok(true),
}
}
/// Abstract operation `ValidateAndApplyPropertyDescriptor`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-validateandapplypropertydescriptor
#[inline]
pub(crate) fn validate_and_apply_property_descriptor(
key: &PropertyKey,
extensible: bool,
desc: PropertyDescriptor,
current: Option<PropertyDescriptor>,
context: &mut Context,
) -> bool {
let _timer = BoaProfiler::global().start_event(
"Object::global_validate_and_apply_property_descriptor",
"object",
);
// 1. Assert: If O is not undefined, then IsPropertyKey(P) is true.
let mut current = if let Some(own) = current {
own
}
// 2. If current is undefined, then
else {
// a. If extensible is false, return false.
if !extensible {
return false;
}
// b. Assert: extensible is true.
context.realm.global_property_map.insert(
key,
// c. If IsGenericDescriptor(Desc) is true or IsDataDescriptor(Desc) is true, then
if desc.is_generic_descriptor() || desc.is_data_descriptor() {
// i. If O is not undefined, create an own data property named P of
// object O whose [[Value]], [[Writable]], [[Enumerable]], and
// [[Configurable]] attribute values are described by Desc.
// If the value of an attribute field of Desc is absent, the attribute
// of the newly created property is set to its default value.
desc.into_data_defaulted()
}
// d. Else,
else {
// i. Assert: ! IsAccessorDescriptor(Desc) is true.
// ii. If O is not undefined, create an own accessor property named P
// of object O whose [[Get]], [[Set]], [[Enumerable]], and [[Configurable]]
// attribute values are described by Desc. If the value of an attribute field
// of Desc is absent, the attribute of the newly created property is set to
// its default value.
desc.into_accessor_defaulted()
},
);
// e. Return true.
return true;
};
// 3. If every field in Desc is absent, return true.
if desc.is_empty() {
return true;
}
// 4. If current.[[Configurable]] is false, then
if !current.expect_configurable() {
// a. If Desc.[[Configurable]] is present and its value is true, return false.
if matches!(desc.configurable(), Some(true)) {
return false;
}
// b. If Desc.[[Enumerable]] is present and ! SameValue(Desc.[[Enumerable]], current.[[Enumerable]])
// is false, return false.
if matches!(desc.enumerable(), Some(desc_enum) if desc_enum != current.expect_enumerable())
{
return false;
}
}
// 5. If ! IsGenericDescriptor(Desc) is true, then
if desc.is_generic_descriptor() {
// a. NOTE: No further validation is required.
}
// 6. Else if ! SameValue(! IsDataDescriptor(current), ! IsDataDescriptor(Desc)) is false, then
else if current.is_data_descriptor() != desc.is_data_descriptor() {
// a. If current.[[Configurable]] is false, return false.
if !current.expect_configurable() {
return false;
}
// b. If IsDataDescriptor(current) is true, then
if current.is_data_descriptor() {
// i. If O is not undefined, convert the property named P of object O from a data
// property to an accessor property. Preserve the existing values of the converted
// property's [[Configurable]] and [[Enumerable]] attributes and set the rest of
// the property's attributes to their default values.
current = current.into_accessor_defaulted();
}
// c. Else,
else {
// i. If O is not undefined, convert the property named P of object O from an
// accessor property to a data property. Preserve the existing values of the
// converted property's [[Configurable]] and [[Enumerable]] attributes and set
// the rest of the property's attributes to their default values.
current = current.into_data_defaulted();
}
}
// 7. Else if IsDataDescriptor(current) and IsDataDescriptor(Desc) are both true, then
else if current.is_data_descriptor() && desc.is_data_descriptor() {
// a. If current.[[Configurable]] is false and current.[[Writable]] is false, then
if !current.expect_configurable() && !current.expect_writable() {
// i. If Desc.[[Writable]] is present and Desc.[[Writable]] is true, return false.
if matches!(desc.writable(), Some(true)) {
return false;
}
// ii. If Desc.[[Value]] is present and SameValue(Desc.[[Value]], current.[[Value]]) is false, return false.
if matches!(desc.value(), Some(value) if !JsValue::same_value(value, current.expect_value()))
{
return false;
}
// iii. Return true.
return true;
}
}
// 8. Else,
// a. Assert: ! IsAccessorDescriptor(current) and ! IsAccessorDescriptor(Desc) are both true.
// b. If current.[[Configurable]] is false, then
else if !current.expect_configurable() {
// i. If Desc.[[Set]] is present and SameValue(Desc.[[Set]], current.[[Set]]) is false, return false.
if matches!(desc.set(), Some(set) if !JsValue::same_value(set, current.expect_set())) {
return false;
}
// ii. If Desc.[[Get]] is present and SameValue(Desc.[[Get]], current.[[Get]]) is false, return false.
if matches!(desc.get(), Some(get) if !JsValue::same_value(get, current.expect_get())) {
return false;
}
// iii. Return true.
return true;
}
// 9. If O is not undefined, then
// a. For each field of Desc that is present, set the corresponding attribute of the
// property named P of object O to the value of the field.
current.fill_with(&desc);
context.realm.global_property_map.insert(key, current);
// 10. Return true.
true
}

1
boa/src/object/internal_methods/mod.rs

@ -19,6 +19,7 @@ pub(super) mod arguments;
pub(super) mod array;
pub(super) mod bound_function;
pub(super) mod function;
pub(crate) mod global;
pub(super) mod integer_indexed;
pub(super) mod proxy;
pub(super) mod string;

3
boa/src/object/mod.rs

@ -39,6 +39,7 @@ use self::internal_methods::{
BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS,
},
function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS},
global::GLOBAL_INTERNAL_METHODS,
integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS,
proxy::{
PROXY_EXOTIC_INTERNAL_METHODS_ALL, PROXY_EXOTIC_INTERNAL_METHODS_BASIC,
@ -339,7 +340,7 @@ impl ObjectData {
pub fn global() -> Self {
Self {
kind: ObjectKind::Global,
internal_methods: &ORDINARY_INTERNAL_METHODS,
internal_methods: &GLOBAL_INTERNAL_METHODS,
}
}

14
boa/src/object/property_map.rs

@ -7,6 +7,10 @@ use indexmap::IndexMap;
use rustc_hash::{FxHashMap, FxHasher};
use std::{collections::hash_map, hash::BuildHasherDefault, iter::FusedIterator};
/// Type alias to make it easier to work with the string properties on the global object.
pub(crate) type GlobalPropertyMap =
IndexMap<JsString, PropertyDescriptor, BuildHasherDefault<FxHasher>>;
/// Wrapper around `indexmap::IndexMap` for usage in `PropertyMap`.
#[derive(Debug, Finalize)]
struct OrderedHashMap<K: Trace>(IndexMap<K, PropertyDescriptor, BuildHasherDefault<FxHasher>>);
@ -180,6 +184,16 @@ impl PropertyMap {
PropertyKey::Symbol(symbol) => self.symbol_properties.0.contains_key(symbol),
}
}
#[inline]
pub(crate) fn string_property_map(&self) -> &GlobalPropertyMap {
&self.string_properties.0
}
#[inline]
pub(crate) fn string_property_map_mut(&mut self) -> &mut GlobalPropertyMap {
&mut self.string_properties.0
}
}
/// An iterator over the property entries of an `Object`

8
boa/src/profiler.rs

@ -31,12 +31,12 @@ impl BoaProfiler {
.start_recording_interval_event(kind, id, thread_id)
}
pub fn default() -> BoaProfiler {
let profiler = Profiler::new(Path::new("./my_trace")).unwrap();
BoaProfiler { profiler }
pub fn default() -> Self {
let profiler = Profiler::new(Path::new("./my_trace")).expect("must be able to create file");
Self { profiler }
}
pub fn global() -> &'static BoaProfiler {
pub fn global() -> &'static Self {
unsafe { INSTANCE.get_or_init(Self::default) }
}

52
boa/src/realm.rs

@ -5,11 +5,8 @@
//! A realm is represented in this implementation as a Realm struct with the fields specified from the spec.
use crate::{
environment::{
global_environment_record::GlobalEnvironmentRecord, lexical_environment::LexicalEnvironment,
},
gc::Gc,
object::{JsObject, ObjectData},
environments::{CompileTimeEnvironmentStack, DeclarativeEnvironmentStack},
object::{GlobalPropertyMap, JsObject, ObjectData, PropertyMap},
BoaProfiler,
};
@ -18,27 +15,50 @@ use crate::{
/// In the specification these are called Realm Records.
#[derive(Debug)]
pub struct Realm {
pub global_object: JsObject,
pub global_env: Gc<GlobalEnvironmentRecord>,
pub environment: LexicalEnvironment,
global_object: JsObject,
pub(crate) global_extensible: bool,
pub(crate) global_property_map: PropertyMap,
pub(crate) environments: DeclarativeEnvironmentStack,
pub(crate) compile_env: CompileTimeEnvironmentStack,
}
impl Realm {
#[allow(clippy::field_reassign_with_default)]
#[inline]
pub fn create() -> Self {
let _timer = BoaProfiler::global().start_event("Realm::create", "realm");
// Create brand new global object
// Global has no prototype to pass None to new_obj
// Allow identification of the global object easily
let gc_global = JsObject::from_proto_and_data(None, ObjectData::global());
// We need to clone the global here because its referenced from separate places (only pointer is cloned)
let global_env = GlobalEnvironmentRecord::new(gc_global.clone(), gc_global.clone());
let global_object = JsObject::from_proto_and_data(None, ObjectData::global());
Self {
global_object: gc_global.clone(),
global_env: Gc::new(global_env),
environment: LexicalEnvironment::new(gc_global),
global_object,
global_extensible: true,
global_property_map: PropertyMap::default(),
environments: DeclarativeEnvironmentStack::new(),
compile_env: CompileTimeEnvironmentStack::new(),
}
}
#[inline]
pub(crate) fn global_object(&self) -> &JsObject {
&self.global_object
}
#[inline]
pub(crate) fn global_bindings(&self) -> &GlobalPropertyMap {
self.global_property_map.string_property_map()
}
#[inline]
pub(crate) fn global_bindings_mut(&mut self) -> &mut GlobalPropertyMap {
self.global_property_map.string_property_map_mut()
}
/// Set the number of bindings on the global environment.
#[inline]
pub(crate) fn set_global_binding_number(&mut self) {
let binding_number = self.compile_env.get_binding_number();
self.environments.set_global_binding_number(binding_number);
}
}

70
boa/src/vm/call_frame.rs

@ -15,11 +15,79 @@ pub struct CallFrame {
pub(crate) finally_return: FinallyReturn,
pub(crate) finally_jump: Vec<Option<u32>>,
pub(crate) pop_on_return: usize,
pub(crate) pop_env_on_return: usize,
// Tracks the number of environments in the current loop block.
// On abrupt returns this is used to decide how many environments need to be pop'ed.
pub(crate) loop_env_stack: Vec<usize>,
// Tracks the number of environments in the current try-catch-finally block.
// On abrupt returns this is used to decide how many environments need to be pop'ed.
pub(crate) try_env_stack: Vec<TryStackEntry>,
pub(crate) param_count: usize,
pub(crate) arg_count: usize,
}
impl CallFrame {
/// Tracks that one environment has been pushed in the current loop block.
pub(crate) fn loop_env_stack_inc(&mut self) {
*self
.loop_env_stack
.last_mut()
.expect("loop environment stack entry must exist") += 1;
}
/// Tracks that one environment has been pop'ed in the current loop block.
pub(crate) fn loop_env_stack_dec(&mut self) {
*self
.loop_env_stack
.last_mut()
.expect("loop environment stack entry must exist") -= 1;
}
/// Tracks that one environment has been pushed in the current try-catch-finally block.
pub(crate) fn try_env_stack_inc(&mut self) {
self.try_env_stack
.last_mut()
.expect("try environment stack entry must exist")
.num_env += 1;
}
/// Tracks that one environment has been pop'ed in the current try-catch-finally block.
pub(crate) fn try_env_stack_dec(&mut self) {
self.try_env_stack
.last_mut()
.expect("try environment stack entry must exist")
.num_env -= 1;
}
/// Tracks that one loop has started in the current try-catch-finally block.
pub(crate) fn try_env_stack_loop_inc(&mut self) {
self.try_env_stack
.last_mut()
.expect("try environment stack entry must exist")
.num_loop_stack_entries += 1;
}
/// Tracks that one loop has finished in the current try-catch-finally block.
pub(crate) fn try_env_stack_loop_dec(&mut self) {
self.try_env_stack
.last_mut()
.expect("try environment stack entry must exist")
.num_loop_stack_entries -= 1;
}
}
/// Tracks the number of environments in the current try-catch-finally block.
///
/// Because of the interactions between loops and try-catch-finally blocks,
/// the number of loop blocks in the try-catch-finally block also needs to be tracked.
#[derive(Copy, Clone, Debug)]
pub(crate) struct TryStackEntry {
pub(crate) num_env: usize,
pub(crate) num_loop_stack_entries: usize,
}
#[derive(Debug)]
pub(crate) struct CatchAddresses {
pub(crate) next: u32,

250
boa/src/vm/code_block.rs

@ -8,10 +8,7 @@ use crate::{
NativeFunctionSignature, ThisMode,
},
context::StandardObjects,
environment::{
function_environment_record::{BindingStatus, FunctionEnvironmentRecord},
lexical_environment::Environment,
},
environments::{BindingLocator, DeclarativeEnvironmentStack},
gc::{Finalize, Gc, Trace},
object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData},
profiler::BoaProfiler,
@ -76,14 +73,25 @@ pub struct CodeBlock {
/// Literals
pub(crate) literals: Vec<JsValue>,
/// Variables names
/// Property field names.
pub(crate) variables: Vec<Sym>,
/// Locators for all bindings in the codeblock.
#[unsafe_ignore_trace]
pub(crate) bindings: Vec<BindingLocator>,
/// Number of binding for the function environment.
pub(crate) num_bindings: usize,
/// Functions inside this function
pub(crate) functions: Vec<Gc<CodeBlock>>,
/// Indicates if the codeblock contains a lexical name `arguments`
pub(crate) lexical_name_argument: bool,
/// The `arguments` binding location of the function, if set.
#[unsafe_ignore_trace]
pub(crate) arguments_binding: Option<BindingLocator>,
}
impl CodeBlock {
@ -93,6 +101,8 @@ impl CodeBlock {
code: Vec::new(),
literals: Vec::new(),
variables: Vec::new(),
bindings: Vec::new(),
num_bindings: 0,
functions: Vec::new(),
name,
length,
@ -101,6 +111,7 @@ impl CodeBlock {
this_mode: ThisMode::Global,
params: Vec::new().into_boxed_slice(),
lexical_name_argument: false,
arguments_binding: None,
}
}
@ -135,7 +146,7 @@ impl CodeBlock {
/// Modifies the `pc` to point to the next instruction.
///
/// Returns an empty `String` if no operands are present.
pub(crate) fn instruction_operands(&self, pc: &mut usize) -> String {
pub(crate) fn instruction_operands(&self, pc: &mut usize, interner: &Interner) -> String {
let opcode: Opcode = self.code[*pc].try_into().expect("invalid opcode");
*pc += size_of::<Opcode>();
match opcode {
@ -175,7 +186,10 @@ impl CodeBlock {
| Opcode::New
| Opcode::NewWithRest
| Opcode::ForInLoopInitIterator
| Opcode::ForInLoopNext => {
| Opcode::ForInLoopNext
| Opcode::ConcatToString
| Opcode::CopyDataProperties
| Opcode::PushDeclarativeEnvironment => {
let result = self.read::<u32>(*pc).to_string();
*pc += size_of::<u32>();
result
@ -192,7 +206,8 @@ impl CodeBlock {
*pc += size_of::<u32>();
format!(
"{operand:04}: '{:?}' (length: {})",
self.functions[operand as usize].name, self.functions[operand as usize].length
interner.resolve_expect(self.functions[operand as usize].name),
self.functions[operand as usize].length
)
}
Opcode::DefInitArg
@ -203,18 +218,27 @@ impl CodeBlock {
| Opcode::DefInitConst
| Opcode::GetName
| Opcode::GetNameOrUndefined
| Opcode::SetName
| Opcode::GetPropertyByName
| Opcode::SetName => {
let operand = self.read::<u32>(*pc);
*pc += size_of::<u32>();
format!(
"{:04}: '{}'",
operand,
interner.resolve_expect(self.bindings[operand as usize].name()),
)
}
Opcode::GetPropertyByName
| Opcode::SetPropertyByName
| Opcode::DefineOwnPropertyByName
| Opcode::SetPropertyGetterByName
| Opcode::SetPropertySetterByName
| Opcode::DeletePropertyByName
| Opcode::ConcatToString
| Opcode::CopyDataProperties => {
| Opcode::DeletePropertyByName => {
let operand = self.read::<u32>(*pc);
*pc += size_of::<u32>();
format!("{operand:04}: '{:?}'", self.variables[operand as usize])
format!(
"{operand:04}: '{}'",
interner.resolve_expect(self.variables[operand as usize]),
)
}
Opcode::Pop
| Opcode::Dup
@ -274,9 +298,11 @@ impl CodeBlock {
| Opcode::FinallyEnd
| Opcode::This
| Opcode::Return
| Opcode::PushDeclarativeEnvironment
| Opcode::PushFunctionEnvironment
| Opcode::PopEnvironment
| Opcode::LoopStart
| Opcode::LoopContinue
| Opcode::LoopEnd
| Opcode::InitIterator
| Opcode::IteratorNext
| Opcode::IteratorNextFull
@ -314,7 +340,7 @@ impl ToInternedString for CodeBlock {
let mut count = 0;
while pc < self.code.len() {
let opcode: Opcode = self.code[pc].try_into().expect("invalid opcode");
let operands = self.instruction_operands(&mut pc);
let operands = self.instruction_operands(&mut pc, interner);
f.push_str(&format!(
" {pc:06} {count:04} {:<27}\n{operands}",
opcode.as_str(),
@ -371,7 +397,7 @@ pub struct JsVmFunction {}
impl JsVmFunction {
#[allow(clippy::new_ret_no_self)]
pub fn new(code: Gc<CodeBlock>, environment: Environment, context: &mut Context) -> JsObject {
pub fn new(code: Gc<CodeBlock>, context: &mut Context) -> JsObject {
let _timer = BoaProfiler::global().start_event("Identifier", "vm");
let function_prototype = context.standard_objects().function_object().prototype();
@ -392,7 +418,10 @@ impl JsVmFunction {
.configurable(true)
.build();
let function = Function::VmOrdinary { code, environment };
let function = Function::VmOrdinary {
code,
environments: context.realm.environments.clone(),
};
let constructor =
JsObject::from_proto_and_data(function_prototype, ObjectData::function(function));
@ -432,7 +461,7 @@ impl JsVmFunction {
pub(crate) enum FunctionBody {
Ordinary {
code: Gc<CodeBlock>,
environment: Environment,
environments: DeclarativeEnvironmentStack,
},
Native {
function: NativeFunctionSignature,
@ -483,9 +512,9 @@ impl JsObject {
function: function.clone(),
captures: captures.clone(),
},
Function::VmOrdinary { code, environment } => FunctionBody::Ordinary {
Function::VmOrdinary { code, environments } => FunctionBody::Ordinary {
code: code.clone(),
environment: environment.clone(),
environments: environments.clone(),
},
}
};
@ -498,71 +527,64 @@ impl JsObject {
FunctionBody::Closure { function, captures } => {
(function)(this, args, captures, context)
}
FunctionBody::Ordinary { code, environment } => {
FunctionBody::Ordinary {
code,
mut environments,
} => {
std::mem::swap(&mut environments, &mut context.realm.environments);
let lexical_this_mode = code.this_mode == ThisMode::Lexical;
// Create a new Function environment whose parent is set to the scope of the function declaration (self.environment)
// <https://tc39.es/ecma262/#sec-prepareforordinarycall>
let local_env = FunctionEnvironmentRecord::new(
this_function_object.clone(),
(!lexical_this_mode).then(|| this.clone()),
Some(environment.clone()),
// Arrow functions do not have a this binding https://tc39.es/ecma262/#sec-function-environment-records
if lexical_this_mode {
BindingStatus::Lexical
let this = if lexical_this_mode {
if let Some(this) = context.realm.environments.get_last_this() {
this
} else {
BindingStatus::Uninitialized
},
JsValue::undefined(),
context,
)?;
// Turn local_env into Environment so it can be cloned
let local_env: Environment = local_env.into();
context.global_object().clone().into()
}
} else if (!code.strict && !context.strict()) && this.is_null_or_undefined() {
context.global_object().clone().into()
} else {
this.clone()
};
// Push the environment first so that it will be used by default parameters
context.push_environment(local_env.clone());
context
.realm
.environments
.push_function(code.num_bindings, this.clone());
let mut arguments_in_parameter_names = false;
let mut is_simple_parameter_list = true;
let mut has_parameter_expressions = false;
let arguments = Sym::ARGUMENTS;
for param in code.params.iter() {
has_parameter_expressions = has_parameter_expressions || param.init().is_some();
arguments_in_parameter_names =
arguments_in_parameter_names || param.names().contains(&arguments);
arguments_in_parameter_names || param.names().contains(&Sym::ARGUMENTS);
is_simple_parameter_list = is_simple_parameter_list
&& !param.is_rest_param()
&& param.is_identifier()
&& param.init().is_none();
}
// An arguments object is added when all of the following conditions are met
// - If not in an arrow function (10.2.11.16)
// - If the parameter list does not contain `arguments` (10.2.11.17)
// - If there are default parameters or if lexical names and function names do not contain `arguments` (10.2.11.18)
//
// https://tc39.es/ecma262/#sec-functiondeclarationinstantiation
if !lexical_this_mode
&& !arguments_in_parameter_names
&& (has_parameter_expressions || !code.lexical_name_argument)
{
// Add arguments object
if let Some(binding) = code.arguments_binding {
let arguments_obj =
if context.strict() || code.strict || !is_simple_parameter_list {
Arguments::create_unmapped_arguments_object(args, context)
} else {
let env = context.realm.environments.current();
Arguments::create_mapped_arguments_object(
&this_function_object,
&code.params,
args,
&local_env,
&env,
context,
)
};
local_env.create_mutable_binding(arguments, false, true, context)?;
local_env.initialize_binding(arguments, arguments_obj.into(), context)?;
context.realm.environments.put_value(
binding.environment_index(),
binding.binding_index(),
arguments_obj.into(),
);
}
let arg_count = args.len();
@ -582,16 +604,6 @@ impl JsObject {
let param_count = code.params.len();
let this = if this.is_null_or_undefined() {
context
.get_global_this_binding()
.expect("global env must have this binding")
} else {
this.to_object(context)
.expect("conversion to object cannot fail here")
.into()
};
context.vm.push_frame(CallFrame {
prev: None,
code,
@ -601,18 +613,25 @@ impl JsObject {
finally_return: FinallyReturn::None,
finally_jump: Vec::new(),
pop_on_return: 0,
pop_env_on_return: 0,
loop_env_stack: vec![0],
try_env_stack: vec![crate::vm::TryStackEntry {
num_env: 0,
num_loop_stack_entries: 0,
}],
param_count,
arg_count,
});
let result = context.run();
context.vm.pop_frame().expect("must have frame");
context.pop_environment();
context.realm.environments.pop();
if has_parameter_expressions {
context.pop_environment();
context.realm.environments.pop();
}
std::mem::swap(&mut environments, &mut context.realm.environments);
result
}
}
@ -645,9 +664,9 @@ impl JsObject {
function: function.clone(),
captures: captures.clone(),
},
Function::VmOrdinary { code, environment } => FunctionBody::Ordinary {
Function::VmOrdinary { code, environments } => FunctionBody::Ordinary {
code: code.clone(),
environment: environment.clone(),
environments: environments.clone(),
},
}
};
@ -657,7 +676,12 @@ impl JsObject {
FunctionBody::Closure { function, captures } => {
(function)(this_target, args, captures, context)
}
FunctionBody::Ordinary { code, environment } => {
FunctionBody::Ordinary {
code,
mut environments,
} => {
std::mem::swap(&mut environments, &mut context.realm.environments);
let this: JsValue = {
// If the prototype of the constructor is not an object, then use the default object
// prototype as prototype for the new object
@ -670,29 +694,11 @@ impl JsObject {
)?;
Self::from_proto_and_data(prototype, ObjectData::ordinary()).into()
};
let lexical_this_mode = code.this_mode == ThisMode::Lexical;
// Create a new Function environment whose parent is set to the scope of the function declaration (self.environment)
// <https://tc39.es/ecma262/#sec-prepareforordinarycall>
let local_env = FunctionEnvironmentRecord::new(
this_function_object.clone(),
Some(this.clone()),
Some(environment),
// Arrow functions do not have a this binding https://tc39.es/ecma262/#sec-function-environment-records
if lexical_this_mode {
BindingStatus::Lexical
} else {
BindingStatus::Uninitialized
},
JsValue::undefined(),
context,
)?;
// Turn local_env into Environment so it can be cloned
let local_env: Environment = local_env.into();
// Push the environment first so that it will be used by default parameters
context.push_environment(local_env.clone());
context
.realm
.environments
.push_function(code.num_bindings, this.clone());
let mut arguments_in_parameter_names = false;
let mut is_simple_parameter_list = true;
@ -708,31 +714,25 @@ impl JsObject {
&& param.init().is_none();
}
// An arguments object is added when all of the following conditions are met
// - If not in an arrow function (10.2.11.16)
// - If the parameter list does not contain `arguments` (10.2.11.17)
// - If there are default parameters or if lexical names and function names do not contain `arguments` (10.2.11.18)
//
// https://tc39.es/ecma262/#sec-functiondeclarationinstantiation
if !lexical_this_mode
&& !arguments_in_parameter_names
&& (has_parameter_expressions || !code.lexical_name_argument)
{
// Add arguments object
if let Some(binding) = code.arguments_binding {
let arguments_obj =
if context.strict() || code.strict || !is_simple_parameter_list {
Arguments::create_unmapped_arguments_object(args, context)
} else {
let env = context.realm.environments.current();
Arguments::create_mapped_arguments_object(
&this_function_object,
&code.params,
args,
&local_env,
&env,
context,
)
};
local_env.create_mutable_binding(Sym::ARGUMENTS, false, true, context)?;
local_env.initialize_binding(Sym::ARGUMENTS, arguments_obj.into(), context)?;
context.realm.environments.put_value(
binding.environment_index(),
binding.binding_index(),
arguments_obj.into(),
);
}
let arg_count = args.len();
@ -752,14 +752,10 @@ impl JsObject {
let param_count = code.params.len();
let this = if this.is_null_or_undefined() {
context
.get_global_this_binding()
.expect("global env must have this binding")
let this = if (!code.strict && !context.strict()) && this.is_null_or_undefined() {
context.global_object().clone().into()
} else {
this.to_object(context)
.expect("conversion to object cannot fail here")
.into()
this
};
context.vm.push_frame(CallFrame {
@ -771,24 +767,34 @@ impl JsObject {
finally_return: FinallyReturn::None,
finally_jump: Vec::new(),
pop_on_return: 0,
pop_env_on_return: 0,
loop_env_stack: vec![0],
try_env_stack: vec![crate::vm::TryStackEntry {
num_env: 0,
num_loop_stack_entries: 0,
}],
param_count,
arg_count,
});
let result = context.run()?;
let result = context.run();
let frame = context.vm.pop_frame().expect("must have frame");
let this = context.get_this_binding();
let this = frame.this;
context.pop_environment();
context.realm.environments.pop();
if has_parameter_expressions {
context.pop_environment();
context.realm.environments.pop();
}
std::mem::swap(&mut environments, &mut context.realm.environments);
let result = result?;
if result.is_object() {
Ok(result)
} else {
this
Ok(this)
}
}
}

397
boa/src/vm/mod.rs

@ -4,12 +4,7 @@
use crate::{
builtins::{iterable::IteratorRecord, Array, ForInIterator, Number},
environment::{
declarative_environment_record::DeclarativeEnvironmentRecord,
function_environment_record::{BindingStatus, FunctionEnvironmentRecord},
lexical_environment::{Environment, VariableScope},
},
property::{PropertyDescriptor, PropertyKey},
property::{DescriptorKind, PropertyDescriptor, PropertyKey},
value::Numeric,
vm::{call_frame::CatchAddresses, code_block::Readable},
BoaProfiler, Context, JsBigInt, JsResult, JsString, JsValue,
@ -22,8 +17,9 @@ mod code_block;
mod opcode;
pub use call_frame::CallFrame;
pub(crate) use call_frame::FinallyReturn;
pub(crate) use call_frame::{FinallyReturn, TryStackEntry};
pub use code_block::{CodeBlock, JsVmFunction};
pub(crate) use opcode::BindingOpcode;
pub use opcode::Opcode;
#[cfg(test)]
@ -308,88 +304,185 @@ impl Context {
Numeric::BigInt(bigint) => self.vm.push(JsBigInt::not(&bigint)),
}
}
Opcode::DefInitArg => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
let value = self.vm.pop();
let local_env = self.get_current_environment();
local_env
.create_mutable_binding(name, false, true, self)
.expect("Failed to create argument binding");
self.initialize_binding(name, value)?;
}
Opcode::DefVar => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
if !self.has_binding(name)? {
self.create_mutable_binding(name, false, VariableScope::Function)?;
self.initialize_binding(name, JsValue::Undefined)?;
let binding_locator = self.vm.frame().code.bindings[index as usize];
if binding_locator.is_global() {
let key = self
.interner()
.resolve_expect(binding_locator.name())
.into();
self.global_bindings_mut().entry(key).or_insert(
PropertyDescriptor::builder()
.value(JsValue::Undefined)
.writable(true)
.enumerable(true)
.configurable(true)
.build(),
);
} else {
self.realm.environments.put_value_if_uninitialized(
binding_locator.environment_index(),
binding_locator.binding_index(),
JsValue::Undefined,
);
}
}
Opcode::DefInitVar => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
let value = self.vm.pop();
if self.has_binding(name)? {
self.set_mutable_binding(name, value, self.strict())?;
let binding_locator = self.vm.frame().code.bindings[index as usize];
binding_locator.throw_mutate_immutable(self)?;
if binding_locator.is_global() {
let key = self
.interner()
.resolve_expect(binding_locator.name())
.into();
crate::object::internal_methods::global::global_set_no_receiver(
&key, value, self,
)?;
} else {
self.create_mutable_binding(name, false, VariableScope::Function)?;
self.initialize_binding(name, value)?;
self.realm.environments.put_value(
binding_locator.environment_index(),
binding_locator.binding_index(),
value,
);
}
}
Opcode::DefLet => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
self.create_mutable_binding(name, false, VariableScope::Block)?;
self.initialize_binding(name, JsValue::Undefined)?;
}
Opcode::DefInitLet => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
let value = self.vm.pop();
self.create_mutable_binding(name, false, VariableScope::Block)?;
self.initialize_binding(name, value)?;
let binding_locator = self.vm.frame().code.bindings[index as usize];
self.realm.environments.put_value(
binding_locator.environment_index(),
binding_locator.binding_index(),
JsValue::Undefined,
);
}
Opcode::DefInitConst => {
Opcode::DefInitLet | Opcode::DefInitConst | Opcode::DefInitArg => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
let value = self.vm.pop();
self.create_immutable_binding(name, true, VariableScope::Block)?;
self.initialize_binding(name, value)?;
let binding_locator = self.vm.frame().code.bindings[index as usize];
self.realm.environments.put_value(
binding_locator.environment_index(),
binding_locator.binding_index(),
value,
);
}
Opcode::GetName => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
let binding_locator = self.vm.frame().code.bindings[index as usize];
binding_locator.throw_mutate_immutable(self)?;
let value = if binding_locator.is_global() {
let key: JsString = self
.interner()
.resolve_expect(binding_locator.name())
.into();
match self.global_bindings_mut().get(&key) {
Some(desc) => match desc.kind() {
DescriptorKind::Data {
value: Some(value), ..
} => value.clone(),
DescriptorKind::Accessor { get: Some(get), .. }
if !get.is_undefined() =>
{
let get = get.clone();
self.call(&get, &self.global_object().clone().into(), &[])?
}
_ => {
return self.throw_reference_error(format!("{key} is not defined"))
}
},
_ => return self.throw_reference_error(format!("{key} is not defined")),
}
} else if let Some(value) = self.realm.environments.get_value_optional(
binding_locator.environment_index(),
binding_locator.binding_index(),
) {
value
} else {
let name =
JsString::from(self.interner().resolve_expect(binding_locator.name()));
return self.throw_reference_error(format!("{name} is not initialized"));
};
let value = self.get_binding_value(name)?;
self.vm.push(value);
}
Opcode::GetNameOrUndefined => {
let index = self.vm.read::<u32>();
let name = self.vm.frame().code.variables[index as usize];
let value = if self.has_binding(name)? {
self.get_binding_value(name)?
let binding_locator = self.vm.frame().code.bindings[index as usize];
binding_locator.throw_mutate_immutable(self)?;
let value = if binding_locator.is_global() {
let key: JsString = self
.interner()
.resolve_expect(binding_locator.name())
.into();
match self.global_bindings_mut().get(&key) {
Some(desc) => match desc.kind() {
DescriptorKind::Data {
value: Some(value), ..
} => value.clone(),
DescriptorKind::Accessor { get: Some(get), .. }
if !get.is_undefined() =>
{
let get = get.clone();
self.call(&get, &self.global_object().clone().into(), &[])?
}
_ => JsValue::undefined(),
},
_ => JsValue::undefined(),
}
} else if let Some(value) = self.realm.environments.get_value_optional(
binding_locator.environment_index(),
binding_locator.binding_index(),
) {
value
} else {
JsValue::Undefined
JsValue::undefined()
};
self.vm.push(value);
}
Opcode::SetName => {
let index = self.vm.read::<u32>();
let binding_locator = self.vm.frame().code.bindings[index as usize];
let value = self.vm.pop();
let name = self.vm.frame().code.variables[index as usize];
binding_locator.throw_mutate_immutable(self)?;
if binding_locator.is_global() {
let key: JsString = self
.interner()
.resolve_expect(binding_locator.name())
.into();
let exists = self.global_bindings_mut().contains_key(&key);
if !exists && (self.strict() || self.vm.frame().code.strict) {
return self
.throw_reference_error(format!("binding already exists: {key}"));
}
self.set_mutable_binding(
name,
let success = crate::object::internal_methods::global::global_set_no_receiver(
&key.clone().into(),
value,
self,
)?;
if !success && (self.strict() || self.vm.frame().code.strict) {
return self
.throw_type_error(format!("cannot set non-writable property: {key}",));
}
} else if !self.realm.environments.put_value_if_initialized(
binding_locator.environment_index(),
binding_locator.binding_index(),
value,
self.strict() || self.vm.frame().code.strict,
)?;
) {
self.throw_reference_error(format!(
"cannot access '{}' before initialization",
self.interner().resolve_expect(binding_locator.name())
))?;
}
}
Opcode::Jump => {
let address = self.vm.read::<u32>();
@ -693,9 +786,32 @@ impl Context {
.push(CatchAddresses { next, finally });
self.vm.frame_mut().finally_jump.push(None);
self.vm.frame_mut().finally_return = FinallyReturn::None;
self.vm.frame_mut().try_env_stack.push(TryStackEntry {
num_env: 0,
num_loop_stack_entries: 0,
});
}
Opcode::TryEnd | Opcode::CatchEnd => {
self.vm.frame_mut().catch.pop();
let try_stack_entry = self.vm.frame_mut().try_env_stack.pop().expect("must exist");
for _ in 0..try_stack_entry.num_env {
self.realm.environments.pop();
}
let mut num_env = try_stack_entry.num_env;
for _ in 0..try_stack_entry.num_loop_stack_entries {
num_env -= self
.vm
.frame_mut()
.loop_env_stack
.pop()
.expect("must exist");
}
*self
.vm
.frame_mut()
.loop_env_stack
.last_mut()
.expect("must exist") -= num_env;
self.vm.frame_mut().finally_return = FinallyReturn::None;
}
Opcode::CatchStart => {
@ -704,6 +820,10 @@ impl Context {
next: finally,
finally: Some(finally),
});
self.vm.frame_mut().try_env_stack.push(TryStackEntry {
num_env: 0,
num_loop_stack_entries: 0,
});
}
Opcode::CatchEnd2 => {
self.vm.frame_mut().finally_return = FinallyReturn::None;
@ -730,14 +850,9 @@ impl Context {
}
}
FinallyReturn::Ok => {
for _ in 0..self.vm.frame().pop_env_on_return {
self.pop_environment();
}
self.vm.frame_mut().pop_env_on_return = 0;
return Ok(true);
}
FinallyReturn::Err => {
self.vm.frame_mut().finally_return = FinallyReturn::None;
return Err(self.vm.pop());
}
}
@ -752,7 +867,7 @@ impl Context {
.expect("finally jump must exist here") = Some(address);
}
Opcode::This => {
let this = self.get_this_binding()?;
let this = self.vm.frame().this.clone();
self.vm.push(this);
}
Opcode::Case => {
@ -774,8 +889,7 @@ impl Context {
Opcode::GetFunction => {
let index = self.vm.read::<u32>();
let code = self.vm.frame().code.functions[index as usize].clone();
let environment = self.get_current_environment();
let function = JsVmFunction::new(code, environment, self);
let function = JsVmFunction::new(code, self);
self.vm.push(function);
}
Opcode::Call => {
@ -798,7 +912,7 @@ impl Context {
};
if this.is_null_or_undefined() {
this = self.global_object().into();
this = self.global_object().clone().into();
}
let result = object.__call__(&this, &arguments, self)?;
@ -836,7 +950,7 @@ impl Context {
};
if this.is_null_or_undefined() {
this = self.global_object().into();
this = self.global_object().clone().into();
}
let result = object.__call__(&this, &arguments, self)?;
@ -900,51 +1014,86 @@ impl Context {
frame.pc = finally_address as usize;
frame.finally_return = FinallyReturn::Ok;
frame.catch.pop();
} else {
for _ in 0..self.vm.frame().pop_env_on_return {
self.pop_environment();
let try_stack_entry =
self.vm.frame_mut().try_env_stack.pop().expect("must exist");
for _ in 0..try_stack_entry.num_env {
self.realm.environments.pop();
}
self.vm.frame_mut().pop_env_on_return = 0;
let mut num_env = try_stack_entry.num_env;
for _ in 0..try_stack_entry.num_loop_stack_entries {
num_env -= self
.vm
.frame_mut()
.loop_env_stack
.pop()
.expect("must exist");
}
*self
.vm
.frame_mut()
.loop_env_stack
.last_mut()
.expect("must exist") -= num_env;
} else {
return Ok(true);
}
}
Opcode::PushDeclarativeEnvironment => {
let env = self.get_current_environment();
self.push_environment(DeclarativeEnvironmentRecord::new(Some(env)));
self.vm.frame_mut().pop_env_on_return += 1;
let num_bindings = self.vm.read::<u32>();
self.realm
.environments
.push_declarative(num_bindings as usize);
self.vm.frame_mut().loop_env_stack_inc();
self.vm.frame_mut().try_env_stack_inc();
}
Opcode::PushFunctionEnvironment => {
let num_bindings = self.vm.read::<u32>();
let is_constructor = self.vm.frame().code.constructor;
let is_lexical = self.vm.frame().code.this_mode.is_lexical();
let current_env = self.get_current_environment();
let this = &self.vm.frame().this;
let new_env = FunctionEnvironmentRecord::new(
this.clone()
.as_object()
.expect("this must always be an object")
.clone(),
if is_constructor || !is_lexical {
Some(this.clone())
} else {
None
},
Some(current_env),
if is_lexical {
BindingStatus::Lexical
} else {
BindingStatus::Uninitialized
},
JsValue::undefined(),
self,
)?;
let this = if is_constructor || !is_lexical {
self.vm.frame().this.clone()
} else {
JsValue::undefined()
};
let new_env: Environment = new_env.into();
self.push_environment(new_env);
self.realm
.environments
.push_function(num_bindings as usize, this);
}
Opcode::PopEnvironment => {
let _env = self.pop_environment();
self.vm.frame_mut().pop_env_on_return -= 1;
self.realm.environments.pop();
self.vm.frame_mut().loop_env_stack_dec();
self.vm.frame_mut().try_env_stack_dec();
}
Opcode::LoopStart => {
self.vm.frame_mut().loop_env_stack.push(0);
self.vm.frame_mut().try_env_stack_loop_inc();
}
Opcode::LoopContinue => {
let env_num = self
.vm
.frame_mut()
.loop_env_stack
.last_mut()
.expect("loop env stack entry must exist");
let env_num_copy = *env_num;
*env_num = 0;
for _ in 0..env_num_copy {
self.realm.environments.pop();
}
}
Opcode::LoopEnd => {
let env_num = self
.vm
.frame_mut()
.loop_env_stack
.pop()
.expect("loop env stack entry must exist");
for _ in 0..env_num {
self.realm.environments.pop();
self.vm.frame_mut().try_env_stack_dec();
}
self.vm.frame_mut().try_env_stack_loop_dec();
}
Opcode::ForInLoopInitIterator => {
let address = self.vm.read::<u32>();
@ -1038,8 +1187,9 @@ impl Context {
let iterator_result = iterator_record.next(self)?;
if iterator_result.done {
self.vm.frame_mut().pc = address as usize;
self.vm.frame_mut().pop_env_on_return -= 1;
self.pop_environment();
self.vm.frame_mut().loop_env_stack_dec();
self.vm.frame_mut().try_env_stack_dec();
self.realm.environments.pop();
self.vm.push(iterator);
self.vm.push(next_function);
} else {
@ -1157,7 +1307,11 @@ impl Context {
.read::<u8>(pc)
.try_into()
.expect("invalid opcode");
let operands = self.vm.frame().code.instruction_operands(&mut pc);
let operands = self
.vm
.frame()
.code
.instruction_operands(&mut pc, self.interner());
let instant = Instant::now();
let result = self.execute_instruction();
@ -1190,17 +1344,40 @@ impl Context {
Ok(should_exit) => {
if should_exit {
let result = self.vm.pop();
self.vm.pop_frame();
return Ok(result);
}
}
Err(e) => {
if let Some(address) = self.vm.frame().catch.last() {
let address = address.next;
if self.vm.frame().pop_env_on_return > 0 {
self.pop_environment();
self.vm.frame_mut().pop_env_on_return -= 1;
let try_stack_entry = self
.vm
.frame_mut()
.try_env_stack
.last_mut()
.expect("must exist");
let try_stack_entry_copy = *try_stack_entry;
try_stack_entry.num_env = 0;
try_stack_entry.num_loop_stack_entries = 0;
for _ in 0..try_stack_entry_copy.num_env {
self.realm.environments.pop();
}
let mut num_env = try_stack_entry_copy.num_env;
for _ in 0..try_stack_entry_copy.num_loop_stack_entries {
num_env -= self
.vm
.frame_mut()
.loop_env_stack
.pop()
.expect("must exist");
}
*self
.vm
.frame_mut()
.loop_env_stack
.last_mut()
.expect("must exist") -= num_env;
self.vm.frame_mut().try_env_stack.pop().expect("must exist");
for _ in 0..self.vm.frame().pop_on_return {
self.vm.pop();
}
@ -1209,11 +1386,6 @@ impl Context {
self.vm.frame_mut().finally_return = FinallyReturn::Err;
self.vm.push(e);
} else {
for _ in 0..self.vm.frame().pop_env_on_return {
self.pop_environment();
}
self.vm.pop_frame();
return Err(e);
}
}
@ -1243,7 +1415,6 @@ impl Context {
println!("\n");
}
self.vm.pop_frame();
if self.vm.stack.is_empty() {
return Ok(JsValue::undefined());
}

41
boa/src/vm/opcode.rs

@ -410,7 +410,7 @@ pub enum Opcode {
///
/// Operands: name_index: `u32`
///
/// Stack: value **=>**
/// Stack: value, has_declarative_binding **=>**
DefInitVar,
/// Declare `let` type variable.
@ -729,7 +729,7 @@ pub enum Opcode {
/// Push a declarative environment.
///
/// Operands:
/// Operands: num_bindings: `u32`
///
/// Stack: **=>**
PushDeclarativeEnvironment,
@ -748,6 +748,27 @@ pub enum Opcode {
/// Stack: **=>**
PopEnvironment,
/// Push loop start marker.
///
/// Operands:
///
/// Stack: **=>**
LoopStart,
/// Clean up environments when a loop continues.
///
/// Operands:
///
/// Stack: **=>**
LoopContinue,
/// Clean up environments at the end of a loop.
///
/// Operands:
///
/// Stack: **=>**
LoopEnd,
/// Initialize the iterator for a for..in loop or jump to after the loop if object is null or undefined.
///
/// Operands: address: `u32`
@ -971,6 +992,9 @@ impl Opcode {
Opcode::PushDeclarativeEnvironment => "PushDeclarativeEnvironment",
Opcode::PushFunctionEnvironment => "PushFunctionEnvironment",
Opcode::PopEnvironment => "PopEnvironment",
Opcode::LoopStart => "LoopStart",
Opcode::LoopContinue => "LoopContinue",
Opcode::LoopEnd => "LoopEnd",
Opcode::ForInLoopInitIterator => "ForInLoopInitIterator",
Opcode::InitIterator => "InitIterator",
Opcode::IteratorNext => "IteratorNext",
@ -1018,3 +1042,16 @@ impl TryFrom<u8> for Opcode {
Ok(opcode)
}
}
/// Specific opcodes for bindings.
///
/// This separate enum exists to make matching exhaustive where needed.
#[derive(Clone, Copy, Debug)]
pub(crate) enum BindingOpcode {
Var,
Let,
InitVar,
InitLet,
InitArg,
InitConst,
}

2
boa_tester/src/exec/js262.rs

@ -7,7 +7,7 @@ use boa::{
/// Initializes the object in the context.
pub(super) fn init(context: &mut Context) -> JsObject {
let global_obj = context.global_object();
let global_obj = context.global_object().clone();
let obj = ObjectInitializer::new(context)
.function(create_realm, "createRealm", 0)

9
boa_tester/src/exec/mod.rs

@ -295,14 +295,7 @@ impl Test {
let mut context = Context::default();
// Register the print() function.
context
.register_global_function("print", 1, test262_print)
.map_err(|e| {
format!(
"could not register the global print() function:\n{}",
e.display()
)
})?;
context.register_global_function("print", 1, test262_print);
// add the $262 object.
let _js262 = js262::init(&mut context);

Loading…
Cancel
Save