From 8721a3167b520681446a6132e5d181c1265ae2e7 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Mon, 9 May 2022 19:09:44 +0000 Subject: [PATCH] Implement the global `eval()` function (#2041) This Pull Request fixes/closes #948. It changes the following: - Implement the global `eval()` function. Runtime code evaluation brings some challenges for environments. Currently the setting and getting of variable bindings is done via indices that are calculated at compile time. This prevents costly hashmap lookups at runtime. Evaluiation at runtime needs access to existing compile time environments. This is a relatively easy change. We wrap compile time environments in `Gc` and make them accessible at runtime. Because `eval()` can add var bindings to existing function environments, we have to adjust the environments for this. Because we cannot recompile all previously stored binding indices, we have to fallback to hashmap lookups at runtime. To prevent this from tanking our performance we add a flag to each environment that marks if any `eval()` has been executed in that environment (or outer environments). This makes it possible to retain the performance of precompiled environment lookups while having a fallback for `eval()`. TLDR: `eval()` is not only horribly unsafe but also a burden for performance. [Never use eval()!](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!) --- boa_engine/src/builtins/eval/mod.rs | 152 +++++++++++ boa_engine/src/builtins/mod.rs | 3 + boa_engine/src/bytecompiler.rs | 270 ++++++++++++------ boa_engine/src/context/mod.rs | 17 ++ boa_engine/src/environments/compile.rs | 361 +++++++++++++++---------- boa_engine/src/environments/mod.rs | 2 +- boa_engine/src/environments/runtime.rs | 278 ++++++++++++++++++- boa_engine/src/realm.rs | 13 +- boa_engine/src/vm/code_block.rs | 67 +++-- boa_engine/src/vm/mod.rs | 248 +++++++++++++---- boa_engine/src/vm/opcode.rs | 22 +- 11 files changed, 1122 insertions(+), 311 deletions(-) create mode 100644 boa_engine/src/builtins/eval/mod.rs diff --git a/boa_engine/src/builtins/eval/mod.rs b/boa_engine/src/builtins/eval/mod.rs new file mode 100644 index 0000000000..f52108f533 --- /dev/null +++ b/boa_engine/src/builtins/eval/mod.rs @@ -0,0 +1,152 @@ +//! This module implements the global `eval` function. +//! +//! The `eval()` function evaluates JavaScript code represented as a string. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-eval-x +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval + +use crate::{ + builtins::{function::Function, BuiltIn, JsArgs}, + object::{JsObject, ObjectData}, + property::Attribute, + Context, JsValue, +}; +use boa_profiler::Profiler; +use rustc_hash::FxHashSet; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct Eval; + +impl BuiltIn for Eval { + const NAME: &'static str = "eval"; + + const ATTRIBUTE: Attribute = Attribute::READONLY + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::PERMANENT); + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let object = JsObject::from_proto_and_data( + context.intrinsics().constructors().function().prototype(), + ObjectData::function(Function::Native { + function: Self::eval, + constructor: false, + }), + ); + + Some(object.into()) + } +} + +impl Eval { + /// `19.2.1 eval ( x )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-eval-x + fn eval(_: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Return ? PerformEval(x, false, false). + Self::perform_eval(args.get_or_undefined(0), false, false, context) + } + + /// `19.2.1.1 PerformEval ( x, strictCaller, direct )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performeval + pub(crate) fn perform_eval( + x: &JsValue, + direct: bool, + strict: bool, + context: &mut Context, + ) -> Result { + // 1. Assert: If direct is false, then strictCaller is also false. + if !direct { + debug_assert!(!strict); + } + + // 2. If Type(x) is not String, return x. + let x = if let Some(x) = x.as_string() { + x.clone() + } else { + return Ok(x.clone()); + }; + + // Because of implementation details the following code differs from the spec. + + // Parse the script body (11.a - 11.d) + // TODO: Implement errors for 11.e - 11.h + let body = match context.parse(x.as_bytes()).map_err(|e| e.to_string()) { + Ok(body) => body, + Err(e) => return context.throw_syntax_error(e), + }; + + // 12 - 13 are implicit in the call of `Context::compile_with_new_declarative`. + + // Because our environment model does not map directly to the spec this section looks very different. + // 14 - 33 are in the following section, together with EvalDeclarationInstantiation. + if direct { + // If the call to eval is direct, the code is executed in the current environment. + + // Poison the current environment, because it may contain new declarations after/during eval. + context.realm.environments.poison_current(); + + // Set the compile time environment to the current running environment and save the number of current environments. + context.realm.compile_env = context.realm.environments.current_compile_environment(); + let environments_len = context.realm.environments.len(); + + // Error if any var declaration in the eval code already exists as a let/const declaration in the current running environment. + let mut vars = FxHashSet::default(); + body.var_declared_names_new(&mut vars); + if let Some(name) = context + .realm + .environments + .has_lex_binding_until_function_environment(&vars) + { + let name = context.interner().resolve_expect(name); + let msg = format!("variable declaration {name} in eval function already exists as lexically declaration"); + return context.throw_syntax_error(msg); + } + + // Compile and execute the eval statement list. + let code_block = context.compile_with_new_declarative(&body, strict)?; + context + .realm + .environments + .extend_outer_function_environment(); + let result = context.execute(code_block); + + // Pop any added runtime environments that where not removed during the eval execution. + context.realm.environments.truncate(environments_len); + + result + } else { + // If the call to eval is indirect, the code is executed in the global environment. + + // Poison all environments, because the global environment may contain new declarations after/during eval. + context.realm.environments.poison_all(); + + // Pop all environments before the eval execution. + let environments = context.realm.environments.pop_to_global(); + let environments_len = context.realm.environments.len(); + context.realm.compile_env = context.realm.environments.current_compile_environment(); + + // Compile and execute the eval statement list. + let code_block = context.compile_with_new_declarative(&body, false)?; + let result = context.execute(code_block); + + // Restore all environments to the state from before the eval execution. + context.realm.environments.truncate(environments_len); + context.realm.environments.extend(environments); + + result + } + } +} diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index ddf02edf2d..7da47d784f 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -9,6 +9,7 @@ pub mod console; pub mod dataview; pub mod date; pub mod error; +pub mod eval; pub mod function; pub mod generator; pub mod generator_function; @@ -41,6 +42,7 @@ pub(crate) use self::{ AggregateError, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, UriError, }, + eval::Eval, function::BuiltInFunctionObject, global_this::GlobalThis, infinity::Infinity, @@ -152,6 +154,7 @@ pub fn init(context: &mut Context) { DataView, Map, Number, + Eval, Set, String, RegExp, diff --git a/boa_engine/src/bytecompiler.rs b/boa_engine/src/bytecompiler.rs index 76419b7a0c..82bef93560 100644 --- a/boa_engine/src/bytecompiler.rs +++ b/boa_engine/src/bytecompiler.rs @@ -1,6 +1,6 @@ use crate::{ builtins::function::ThisMode, - environments::BindingLocator, + environments::{BindingLocator, CompileTimeEnvironment}, syntax::ast::{ node::{ declaration::{ @@ -19,7 +19,7 @@ use crate::{ vm::{BindingOpcode, CodeBlock, Opcode}, Context, JsBigInt, JsResult, JsString, JsValue, }; -use boa_gc::Gc; +use boa_gc::{Cell, Gc}; use boa_interner::{Interner, Sym}; use rustc_hash::FxHashMap; use std::mem::size_of; @@ -95,6 +95,14 @@ impl<'b> ByteCompiler<'b> { self.context.interner() } + /// Push a compile time environment to the current `CodeBlock` and return it's index. + #[inline] + fn push_compile_environment(&mut self, environment: Gc>) -> usize { + let index = self.code_block.compile_environments.len(); + self.code_block.compile_environments.push(environment); + index + } + #[inline] fn get_or_insert_literal(&mut self, literal: Literal) -> u32 { if let Some(index) = self.literals_map.get(&literal) { @@ -281,13 +289,24 @@ impl<'b> ByteCompiler<'b> { Label { index } } + /// Emit an opcode with a dummy operand. + /// Return the `Label` of the operand. #[inline] - fn jump_with_custom_opcode(&mut self, opcode: Opcode) -> Label { + fn emit_opcode_with_operand(&mut self, opcode: Opcode) -> Label { let index = self.next_opcode_location(); self.emit(opcode, &[Self::DUMMY_ADDRESS]); Label { index } } + /// Emit an opcode with two dummy operands. + /// Return the `Label`s of the two operands. + #[inline] + fn emit_opcode_with_two_operands(&mut self, opcode: Opcode) -> (Label, Label) { + let index = self.next_opcode_location(); + self.emit(opcode, &[Self::DUMMY_ADDRESS, Self::DUMMY_ADDRESS]); + (Label { index }, Label { index: index + 4 }) + } + #[inline] fn patch_jump_with_target(&mut self, label: Label, target: u32) { let Label { index } = label; @@ -551,6 +570,35 @@ impl<'b> ByteCompiler<'b> { Ok(()) } + /// Compile a statement list in a new declarative environment. + #[inline] + pub(crate) fn compile_statement_list_with_new_declarative( + &mut self, + list: &[Node], + use_expr: bool, + strict: bool, + ) -> JsResult<()> { + self.context.push_compile_time_environment(strict); + let push_env = self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); + + self.create_declarations(list)?; + + if let Some((last, items)) = list.split_last() { + for node in items { + self.compile_stmt(node, false)?; + } + self.compile_stmt(last, use_expr)?; + } + + let (num_bindings, compile_environment) = self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); + self.emit_opcode(Opcode::PopEnvironment); + + Ok(()) + } + #[inline] pub fn compile_expr(&mut self, expr: &Node, use_expr: bool) -> JsResult<()> { match expr { @@ -726,17 +774,17 @@ impl<'b> ByteCompiler<'b> { BinOp::Log(op) => { match op { LogOp::And => { - let exit = self.jump_with_custom_opcode(Opcode::LogicalAnd); + let exit = self.emit_opcode_with_operand(Opcode::LogicalAnd); self.compile_expr(binary.rhs(), true)?; self.patch_jump(exit); } LogOp::Or => { - let exit = self.jump_with_custom_opcode(Opcode::LogicalOr); + let exit = self.emit_opcode_with_operand(Opcode::LogicalOr); self.compile_expr(binary.rhs(), true)?; self.patch_jump(exit); } LogOp::Coalesce => { - let exit = self.jump_with_custom_opcode(Opcode::Coalesce); + let exit = self.emit_opcode_with_operand(Opcode::Coalesce); self.compile_expr(binary.rhs(), true)?; self.patch_jump(exit); } @@ -761,7 +809,7 @@ impl<'b> ByteCompiler<'b> { AssignOp::Shr => Some(Opcode::ShiftRight), AssignOp::Ushr => Some(Opcode::UnsignedShiftRight), AssignOp::BoolAnd => { - let exit = self.jump_with_custom_opcode(Opcode::LogicalAnd); + let exit = self.emit_opcode_with_operand(Opcode::LogicalAnd); self.compile_expr(binary.rhs(), true)?; let access = Self::compile_access(binary.lhs()).ok_or_else(|| { @@ -774,7 +822,7 @@ impl<'b> ByteCompiler<'b> { None } AssignOp::BoolOr => { - let exit = self.jump_with_custom_opcode(Opcode::LogicalOr); + let exit = self.emit_opcode_with_operand(Opcode::LogicalOr); self.compile_expr(binary.rhs(), true)?; let access = Self::compile_access(binary.lhs()).ok_or_else(|| { @@ -787,7 +835,7 @@ impl<'b> ByteCompiler<'b> { None } AssignOp::Coalesce => { - let exit = self.jump_with_custom_opcode(Opcode::Coalesce); + let exit = self.emit_opcode_with_operand(Opcode::Coalesce); self.compile_expr(binary.rhs(), true)?; let access = Self::compile_access(binary.lhs()).ok_or_else(|| { @@ -1067,7 +1115,7 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::InitIterator); self.emit_opcode(Opcode::PushUndefined); let start_address = self.next_opcode_location(); - let start = self.jump_with_custom_opcode(Opcode::GeneratorNextDelegate); + let start = self.emit_opcode_with_operand(Opcode::GeneratorNextDelegate); self.emit(Opcode::Jump, &[start_address]); self.patch_jump(start); } else { @@ -1260,7 +1308,8 @@ impl<'b> ByteCompiler<'b> { } Node::ForLoop(for_loop) => { self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); if let Some(init) = for_loop.init() { self.create_decls_from_stmt(init)?; @@ -1298,13 +1347,16 @@ impl<'b> ByteCompiler<'b> { self.pop_loop_control_info(); self.emit_opcode(Opcode::LoopEnd); - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); } Node::ForInLoop(for_in_loop) => { self.compile_expr(for_in_loop.expr(), true)?; - let early_exit = self.jump_with_custom_opcode(Opcode::ForInLoopInitIterator); + let early_exit = self.emit_opcode_with_operand(Opcode::ForInLoopInitIterator); self.emit_opcode(Opcode::LoopStart); let start_address = self.next_opcode_location(); @@ -1312,8 +1364,9 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::LoopContinue); self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); - let exit = self.jump_with_custom_opcode(Opcode::ForInLoopNext); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); + let exit = self.emit_opcode_with_operand(Opcode::ForInLoopNext); match for_in_loop.init() { IterableLoopInitializer::Identifier(ref ident) => { @@ -1368,8 +1421,11 @@ impl<'b> ByteCompiler<'b> { self.compile_stmt(for_in_loop.body(), false)?; - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); self.emit(Opcode::Jump, &[start_address]); @@ -1392,8 +1448,9 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::LoopContinue); self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); - let exit = self.jump_with_custom_opcode(Opcode::ForInLoopNext); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); + let exit = self.emit_opcode_with_operand(Opcode::ForInLoopNext); match for_of_loop.init() { IterableLoopInitializer::Identifier(ref ident) => { @@ -1448,8 +1505,11 @@ impl<'b> ByteCompiler<'b> { self.compile_stmt(for_of_loop.body(), false)?; - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); self.emit(Opcode::Jump, &[start_address]); @@ -1623,11 +1683,15 @@ impl<'b> ByteCompiler<'b> { } Node::Block(block) => { self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); self.create_declarations(block.items())?; self.compile_statement_list(block.items(), use_expr)?; - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); } Node::Throw(throw) => { @@ -1636,7 +1700,8 @@ impl<'b> ByteCompiler<'b> { } Node::Switch(switch) => { self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); for case in switch.cases() { self.create_declarations(case.body().items())?; } @@ -1649,10 +1714,10 @@ impl<'b> ByteCompiler<'b> { let mut labels = Vec::with_capacity(switch.cases().len()); for case in switch.cases() { self.compile_expr(case.condition(), true)?; - labels.push(self.jump_with_custom_opcode(Opcode::Case)); + labels.push(self.emit_opcode_with_operand(Opcode::Case)); } - let exit = self.jump_with_custom_opcode(Opcode::Default); + let exit = self.emit_opcode_with_operand(Opcode::Default); for (label, case) in labels.into_iter().zip(switch.cases()) { self.patch_jump(label); @@ -1668,8 +1733,12 @@ impl<'b> ByteCompiler<'b> { self.pop_switch_control_info(); self.emit_opcode(Opcode::LoopEnd); - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); } Node::FunctionDecl(_function) => self.function(node, false)?, @@ -1686,13 +1755,17 @@ impl<'b> ByteCompiler<'b> { let try_start = self.next_opcode_location(); self.emit(Opcode::TryStart, &[Self::DUMMY_ADDRESS, 0]); self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); self.create_declarations(t.block().items())?; self.compile_statement_list(t.block().items(), use_expr)?; - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); self.emit_opcode(Opcode::TryEnd); @@ -1702,12 +1775,13 @@ impl<'b> ByteCompiler<'b> { if let Some(catch) = t.catch() { self.push_try_control_info_catch_start(); let catch_start = if t.finally().is_some() { - Some(self.jump_with_custom_opcode(Opcode::CatchStart)) + Some(self.emit_opcode_with_operand(Opcode::CatchStart)) } else { None }; self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); if let Some(decl) = catch.parameter() { match decl { Declaration::Identifier { ident, .. } => { @@ -1728,8 +1802,12 @@ impl<'b> ByteCompiler<'b> { self.create_declarations(catch.block().items())?; self.compile_statement_list(catch.block().items(), use_expr)?; - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = + self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); if let Some(catch_start) = catch_start { self.emit_opcode(Opcode::CatchEnd); @@ -1755,13 +1833,18 @@ impl<'b> ByteCompiler<'b> { ); self.context.push_compile_time_environment(false); - let push_env = self.jump_with_custom_opcode(Opcode::PushDeclarativeEnvironment); + let push_env = + self.emit_opcode_with_two_operands(Opcode::PushDeclarativeEnvironment); self.create_declarations(finally.items())?; self.compile_statement_list(finally.items(), false)?; - let num_bindings = self.context.pop_compile_time_environment().num_bindings(); - self.patch_jump_with_target(push_env, num_bindings as u32); + let (num_bindings, compile_environment) = + self.context.pop_compile_time_environment(); + let index_compile_environment = + self.push_compile_environment(compile_environment); + self.patch_jump_with_target(push_env.0, num_bindings as u32); + self.patch_jump_with_target(push_env.1, index_compile_environment as u32); self.emit_opcode(Opcode::PopEnvironment); self.emit_opcode(Opcode::FinallyEnd); @@ -1879,7 +1962,7 @@ impl<'b> ByteCompiler<'b> { Declaration::Identifier { ident, .. } => { compiler.context.create_mutable_binding(ident.sym(), false); if let Some(init) = parameter.declaration().init() { - let skip = compiler.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip = compiler.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); compiler.compile_expr(init, true)?; compiler.patch_jump(skip); } @@ -1901,7 +1984,7 @@ impl<'b> ByteCompiler<'b> { let env_label = if parameters.has_expressions() { compiler.code_block.num_bindings = compiler.context.get_binding_number(); compiler.context.push_compile_time_environment(true); - Some(compiler.jump_with_custom_opcode(Opcode::PushFunctionEnvironment)) + Some(compiler.emit_opcode_with_two_operands(Opcode::PushFunctionEnvironment)) } else { None }; @@ -1916,17 +1999,22 @@ impl<'b> ByteCompiler<'b> { compiler.compile_statement_list(body.items(), false)?; if let Some(env_label) = env_label { - let num_bindings = compiler - .context - .pop_compile_time_environment() - .num_bindings(); - compiler.patch_jump_with_target(env_label, num_bindings as u32); - compiler.context.pop_compile_time_environment(); + let (num_bindings, compile_environment) = + compiler.context.pop_compile_time_environment(); + let index_compile_environment = compiler.push_compile_environment(compile_environment); + compiler.patch_jump_with_target(env_label.0, num_bindings as u32); + compiler.patch_jump_with_target(env_label.1, index_compile_environment as u32); + + let (_, compile_environment) = compiler.context.pop_compile_time_environment(); + compiler.push_compile_environment(compile_environment); } else { - compiler.code_block.num_bindings = compiler - .context - .pop_compile_time_environment() - .num_bindings(); + let (num_bindings, compile_environment) = + compiler.context.pop_compile_time_environment(); + compiler + .code_block + .compile_environments + .push(compile_environment); + compiler.code_block.num_bindings = num_bindings; } compiler.code_block.params = parameters.clone(); @@ -1966,12 +2054,16 @@ impl<'b> ByteCompiler<'b> { pub(crate) fn call(&mut self, node: &Node, use_expr: bool) -> JsResult<()> { #[derive(PartialEq)] enum CallKind { + CallEval, Call, New, } let (call, kind) = match node { - Node::Call(call) => (call, CallKind::Call), + Node::Call(call) => match call.expr() { + Node::Identifier(ident) if ident.sym() == Sym::EVAL => (call, CallKind::CallEval), + _ => (call, CallKind::Call), + }, Node::New(new) => (new.call(), CallKind::New), _ => unreachable!(), }; @@ -1996,7 +2088,7 @@ impl<'b> ByteCompiler<'b> { } expr => { self.compile_expr(expr, true)?; - if kind == CallKind::Call { + if kind == CallKind::Call || kind == CallKind::CallEval { self.emit_opcode(Opcode::This); self.emit_opcode(Opcode::Swap); } @@ -2010,6 +2102,10 @@ impl<'b> ByteCompiler<'b> { let last_is_rest_parameter = matches!(call.args().last(), Some(Node::Spread(_))); match kind { + CallKind::CallEval if last_is_rest_parameter => { + self.emit(Opcode::CallEvalWithRest, &[call.args().len() as u32]); + } + CallKind::CallEval => self.emit(Opcode::CallEval, &[call.args().len() as u32]), CallKind::Call if last_is_rest_parameter => { self.emit(Opcode::CallWithRest, &[call.args().len() as u32]); } @@ -2039,7 +2135,7 @@ impl<'b> ByteCompiler<'b> { ) -> JsResult<()> { match pattern { DeclarationPattern::Object(pattern) => { - let skip_init = self.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip_init = self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); if let Some(init) = pattern.init() { self.compile_expr(init, true)?; } else { @@ -2078,7 +2174,8 @@ impl<'b> ByteCompiler<'b> { } if let Some(init) = default_init { - let skip = self.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip = + self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); self.compile_expr(init, true)?; self.patch_jump(skip); } @@ -2140,7 +2237,8 @@ impl<'b> ByteCompiler<'b> { } if let Some(init) = default_init { - let skip = self.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip = + self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); self.compile_expr(init, true)?; self.patch_jump(skip); } @@ -2153,7 +2251,7 @@ impl<'b> ByteCompiler<'b> { self.emit_opcode(Opcode::Pop); } DeclarationPattern::Array(pattern) => { - let skip_init = self.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip_init = self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); if let Some(init) = pattern.init() { self.compile_expr(init, true)?; } else { @@ -2192,7 +2290,8 @@ impl<'b> ByteCompiler<'b> { } => { self.emit_opcode(next); if let Some(init) = default_init { - let skip = self.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip = + self.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); self.compile_expr(init, true)?; self.patch_jump(skip); } @@ -2462,7 +2561,8 @@ impl<'b> ByteCompiler<'b> { Declaration::Identifier { ident, .. } => { compiler.context.create_mutable_binding(ident.sym(), false); if let Some(init) = parameter.declaration().init() { - let skip = compiler.jump_with_custom_opcode(Opcode::JumpIfNotUndefined); + let skip = + compiler.emit_opcode_with_operand(Opcode::JumpIfNotUndefined); compiler.compile_expr(init, true)?; compiler.patch_jump(skip); } @@ -2482,30 +2582,38 @@ impl<'b> ByteCompiler<'b> { let env_label = if expr.parameters().has_expressions() { compiler.code_block.num_bindings = compiler.context.get_binding_number(); compiler.context.push_compile_time_environment(true); - Some(compiler.jump_with_custom_opcode(Opcode::PushFunctionEnvironment)) + Some(compiler.emit_opcode_with_two_operands(Opcode::PushFunctionEnvironment)) } else { None }; compiler.create_declarations(expr.body().items())?; compiler.compile_statement_list(expr.body().items(), false)?; if let Some(env_label) = env_label { - let num_bindings = compiler - .context - .pop_compile_time_environment() - .num_bindings(); - compiler.patch_jump_with_target(env_label, num_bindings as u32); - compiler.context.pop_compile_time_environment(); + let (num_bindings, compile_environment) = + compiler.context.pop_compile_time_environment(); + let index_compile_environment = + compiler.push_compile_environment(compile_environment); + compiler.patch_jump_with_target(env_label.0, num_bindings as u32); + compiler.patch_jump_with_target(env_label.1, index_compile_environment as u32); + let (_, compile_environment) = compiler.context.pop_compile_time_environment(); + compiler.push_compile_environment(compile_environment); } else { - compiler.code_block.num_bindings = compiler - .context - .pop_compile_time_environment() - .num_bindings(); + let (num_bindings, compile_environment) = + compiler.context.pop_compile_time_environment(); + compiler + .code_block + .compile_environments + .push(compile_environment); + compiler.code_block.num_bindings = num_bindings; } } else { - compiler.code_block.num_bindings = compiler - .context - .pop_compile_time_environment() - .num_bindings(); + let (num_bindings, compile_environment) = + compiler.context.pop_compile_time_environment(); + compiler + .code_block + .compile_environments + .push(compile_environment); + compiler.code_block.num_bindings = num_bindings; } compiler.emit_opcode(Opcode::PushUndefined); @@ -2655,16 +2763,20 @@ impl<'b> ByteCompiler<'b> { compiler.context.push_compile_time_environment(true); compiler.create_declarations(statement_list.items())?; compiler.compile_statement_list(statement_list.items(), false)?; - compiler.code_block.num_bindings = compiler - .context - .pop_compile_time_environment() - .num_bindings(); + let (num_bindings, compile_environment) = + compiler.context.pop_compile_time_environment(); + compiler + .code_block + .compile_environments + .push(compile_environment); + compiler.code_block.num_bindings = num_bindings; let code = Gc::new(compiler.finish()); let index = self.code_block.functions.len() as u32; self.code_block.functions.push(code); self.emit(Opcode::GetFunction, &[index]); self.emit(Opcode::Call, &[0]); + self.emit_opcode(Opcode::Pop); } ClassElement::MethodDefinition(..) | ClassElement::PrivateMethodDefinition(..) diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index da4088cabc..813bf6d8f0 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -670,6 +670,23 @@ impl Context { Ok(Gc::new(compiler.finish())) } + /// Compile the AST into a `CodeBlock` with an additional declarative environment. + #[inline] + pub(crate) fn compile_with_new_declarative( + &mut self, + statement_list: &StatementList, + strict: bool, + ) -> JsResult> { + let _timer = Profiler::global().start_event("Compilation", "Main"); + let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), self); + compiler.compile_statement_list_with_new_declarative( + statement_list.items(), + true, + strict || statement_list.strict(), + )?; + Ok(Gc::new(compiler.finish())) + } + /// Call the VM with a `CodeBlock` and return the result. /// /// Since this function receives a `Gc`, cloning the code is very cheap, since it's diff --git a/boa_engine/src/environments/compile.rs b/boa_engine/src/environments/compile.rs index 28e3be12d3..9bea4ba6f2 100644 --- a/boa_engine/src/environments/compile.rs +++ b/boa_engine/src/environments/compile.rs @@ -1,6 +1,7 @@ use crate::{ environments::runtime::BindingLocator, property::PropertyDescriptor, Context, JsString, JsValue, }; +use boa_gc::{Cell, Finalize, Gc, Trace}; use boa_interner::Sym; use rustc_hash::FxHashMap; @@ -11,60 +12,195 @@ use rustc_hash::FxHashMap; struct CompileTimeBinding { index: usize, mutable: bool, + lex: 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)] +#[derive(Debug, Finalize, Trace)] pub(crate) struct CompileTimeEnvironment { + outer: Option>>, + environment_index: usize, + #[unsafe_ignore_trace] bindings: FxHashMap, function_scope: bool, } impl CompileTimeEnvironment { + /// Crate a new global compile time environment. + #[inline] + pub(crate) fn new_global() -> Self { + Self { + outer: None, + environment_index: 0, + bindings: FxHashMap::default(), + function_scope: true, + } + } + + /// Check if environment has a lexical binding with the given name. + #[inline] + pub(crate) fn has_lex_binding(&self, name: Sym) -> bool { + self.bindings + .get(&name) + .map_or(false, |binding| binding.lex) + } + /// 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, -} + /// Check if the environment is a function environment. + #[inline] + pub(crate) fn is_function(&self) -> bool { + self.function_scope + } + + /// Get the locator for a binding name. + #[inline] + pub(crate) fn get_binding(&self, name: Sym) -> Option { + self.bindings + .get(&name) + .map(|binding| BindingLocator::declarative(name, self.environment_index, binding.index)) + } + + /// Get the locator for a binding name in this and all outer environments. + #[inline] + pub(crate) fn get_binding_recursive(&self, name: Sym) -> BindingLocator { + if let Some(binding) = self.bindings.get(&name) { + BindingLocator::declarative(name, self.environment_index, binding.index) + } else if let Some(outer) = &self.outer { + outer.borrow().get_binding_recursive(name) + } else { + BindingLocator::global(name) + } + } -impl CompileTimeEnvironmentStack { - /// Creates a new compile time environment stack. + /// Check if a binding name exists in this and all outer environments. + #[inline] + pub(crate) fn has_binding_recursive(&self, name: Sym) -> bool { + if self.bindings.contains_key(&name) { + true + } else if let Some(outer) = &self.outer { + outer.borrow().has_binding_recursive(name) + } else { + false + } + } + + /// Create a mutable binding. /// - /// This function should only be used once, on realm creation. + /// If the binding is a function scope binding and this is a declarative environment, try the outer environment. #[inline] - pub(crate) fn new() -> Self { - Self { - stack: vec![CompileTimeEnvironment { - bindings: FxHashMap::default(), - function_scope: true, - }], + pub(crate) fn create_mutable_binding(&mut self, name: Sym, function_scope: bool) -> bool { + if let Some(outer) = &self.outer { + if !function_scope || self.function_scope { + if !self.bindings.contains_key(&name) { + let binding_index = self.bindings.len(); + self.bindings.insert( + name, + CompileTimeBinding { + index: binding_index, + mutable: true, + lex: !function_scope, + }, + ); + } + true + } else { + return outer + .borrow_mut() + .create_mutable_binding(name, function_scope); + } + } else if function_scope { + false + } else { + if !self.bindings.contains_key(&name) { + let binding_index = self.bindings.len(); + self.bindings.insert( + name, + CompileTimeBinding { + index: binding_index, + mutable: true, + lex: !function_scope, + }, + ); + } + true + } + } + + /// Crate an immutable binding. + #[inline] + pub(crate) fn create_immutable_binding(&mut self, name: Sym) { + let binding_index = self.bindings.len(); + self.bindings.insert( + name, + CompileTimeBinding { + index: binding_index, + mutable: false, + lex: true, + }, + ); + } + + /// Return the binding locator for a mutable binding with the given binding name and scope. + #[inline] + pub(crate) fn initialize_mutable_binding( + &self, + name: Sym, + function_scope: bool, + ) -> BindingLocator { + if let Some(outer) = &self.outer { + if function_scope && !self.function_scope { + return outer + .borrow() + .initialize_mutable_binding(name, function_scope); + } + if let Some(binding) = self.bindings.get(&name) { + BindingLocator::declarative(name, self.environment_index, binding.index) + } else { + outer + .borrow() + .initialize_mutable_binding(name, function_scope) + } + } else if let Some(binding) = self.bindings.get(&name) { + BindingLocator::declarative(name, self.environment_index, binding.index) + } else { + BindingLocator::global(name) } } - /// Get the number of bindings for the current last environment. + /// Return the binding locator for an immutable binding. /// /// # Panics /// - /// Panics if there are no environments on the stack. + /// Panics if the binding is not in the current environment. #[inline] - pub(crate) fn get_binding_number(&self) -> usize { - self.stack - .last() - .expect("global environment must always exist") - .num_bindings() + pub(crate) fn initialize_immutable_binding(&self, name: Sym) -> BindingLocator { + let binding = self.bindings.get(&name).expect("binding must exist"); + BindingLocator::declarative(name, self.environment_index, binding.index) + } + + /// Return the binding locator for a mutable binding. + #[inline] + pub(crate) fn set_mutable_binding_recursive(&self, name: Sym) -> BindingLocator { + match self.bindings.get(&name) { + Some(binding) if binding.mutable => { + BindingLocator::declarative(name, self.environment_index, binding.index) + } + Some(_) => BindingLocator::mutate_immutable(name), + None => { + if let Some(outer) = &self.outer { + outer.borrow().set_mutable_binding_recursive(name) + } else { + BindingLocator::global(name) + } + } + } } } @@ -74,10 +210,15 @@ impl Context { /// 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 { + let environment_index = self.realm.compile_env.borrow().environment_index + 1; + let outer = self.realm.compile_env.clone(); + + self.realm.compile_env = Gc::new(Cell::new(CompileTimeEnvironment { + outer: Some(outer), + environment_index, bindings: FxHashMap::default(), function_scope, - }); + })); } /// Pop the last compile time environment from the stack. @@ -88,16 +229,20 @@ impl Context { /// /// 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") + pub(crate) fn pop_compile_time_environment( + &mut self, + ) -> (usize, Gc>) { + let current_env_borrow = self.realm.compile_env.borrow(); + if let Some(outer) = ¤t_env_borrow.outer { + let outer_clone = outer.clone(); + let num_bindings = current_env_borrow.num_bindings(); + drop(current_env_borrow); + let current = self.realm.compile_env.clone(); + self.realm.compile_env = outer_clone; + (num_bindings, current) + } else { + panic!("cannot pop global environment") + } } /// Get the number of bindings for the current compile time environment. @@ -109,12 +254,7 @@ impl Context { /// 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() + self.realm.compile_env.borrow().num_bindings() } /// Get the binding locator of the binding at bytecode compile time. @@ -122,12 +262,7 @@ impl Context { /// 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) + self.realm.compile_env.borrow().get_binding_recursive(name) } /// Return if a declarative binding exists at bytecode compile time. @@ -136,12 +271,7 @@ impl Context { /// 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 + self.realm.compile_env.borrow().has_binding_recursive(name) } /// Create a mutable binding at bytecode compile time. @@ -154,49 +284,30 @@ impl Context { /// Panics if the global environment is not function scoped. #[inline] pub(crate) fn create_mutable_binding(&mut self, name: Sym, function_scope: bool) { - 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) { - return; - } - - if i == 0 { - let desc = self - .realm - .global_property_map - .string_property_map() - .get(&name_str); - 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; - } else if function_scope { - return; - } - } - - let binding_index = env.bindings.len(); - env.bindings.insert( - name, - CompileTimeBinding { - index: binding_index, - mutable: true, - }, + if !self + .realm + .compile_env + .borrow_mut() + .create_mutable_binding(name, function_scope) + { + let name_str = JsString::from(self.interner().resolve_expect(name)); + let desc = self + .realm + .global_property_map + .string_property_map() + .get(&name_str); + if desc.is_none() { + self.global_bindings_mut().insert( + name_str, + PropertyDescriptor::builder() + .value(JsValue::Undefined) + .writable(true) + .enumerable(true) + .configurable(true) + .build(), ); - return; } - continue; } - panic!("global environment must be function scoped") } /// Initialize a mutable binding at bytecode compile time and return it's binding locator. @@ -208,16 +319,10 @@ impl Context { 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) + self.realm + .compile_env + .borrow() + .initialize_mutable_binding(name, function_scope) } /// Create an immutable binding at bytecode compile time. @@ -230,21 +335,10 @@ impl Context { /// Panics if the global environment does not exist. #[inline] pub(crate) fn create_immutable_binding(&mut self, name: Sym) { - let env = self - .realm + self.realm .compile_env - .stack - .last_mut() - .expect("global environment must always exist"); - - let binding_index = env.bindings.len(); - env.bindings.insert( - name, - CompileTimeBinding { - index: binding_index, - mutable: false, - }, - ); + .borrow_mut() + .create_immutable_binding(name); } /// Initialize an immutable binding at bytecode compile time and return it's binding locator. @@ -256,16 +350,10 @@ impl Context { /// 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 + 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) + .borrow() + .initialize_immutable_binding(name) } /// Return the binding locator for a set operation on an existing binding. @@ -273,14 +361,9 @@ impl Context { /// 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) + self.realm + .compile_env + .borrow() + .set_mutable_binding_recursive(name) } } diff --git a/boa_engine/src/environments/mod.rs b/boa_engine/src/environments/mod.rs index b33b65ab7f..a1f88c3b42 100644 --- a/boa_engine/src/environments/mod.rs +++ b/boa_engine/src/environments/mod.rs @@ -28,7 +28,7 @@ mod compile; mod runtime; pub(crate) use { - compile::CompileTimeEnvironmentStack, + compile::CompileTimeEnvironment, runtime::{BindingLocator, DeclarativeEnvironment, DeclarativeEnvironmentStack}, }; diff --git a/boa_engine/src/environments/runtime.rs b/boa_engine/src/environments/runtime.rs index 1e2c3dd7e7..8842386a7b 100644 --- a/boa_engine/src/environments/runtime.rs +++ b/boa_engine/src/environments/runtime.rs @@ -1,18 +1,34 @@ -use crate::{Context, JsResult, JsValue}; +use crate::{environments::CompileTimeEnvironment, Context, JsResult, JsValue}; use boa_gc::{Cell, Finalize, Gc, Trace}; use boa_interner::Sym; +use rustc_hash::FxHashSet; -/// A declarative environment holds the bindings values at runtime. +/// A declarative environment holds binding 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. +/// +/// Code evaluation at runtime (e.g. the `eval` built-in function) can add +/// bindings to existing, compiled function environments. +/// This makes it impossible to determine the location of all bindings at compile time. +/// To dynamically check for added bindings at runtime, a reference to the +/// corresponding compile time environment is needed. +/// +/// Checking all environments for potential added bindings at runtime on every get/set +/// would offset the performance improvement of determining binding locations at compile time. +/// To minimize this, each environment holds a `poisoned` flag. +/// If bindings where added at runtime, the current environment and all inner environments +/// are marked as poisoned. +/// All poisoned environments have to be checked for added bindings. #[derive(Debug, Trace, Finalize)] pub(crate) struct DeclarativeEnvironment { bindings: Cell>>, this: Option, + compile: Gc>, + poisoned: Cell, } impl DeclarativeEnvironment { @@ -59,15 +75,77 @@ pub struct DeclarativeEnvironmentStack { impl DeclarativeEnvironmentStack { /// Create a new environment stack with the most outer declarative environment. #[inline] - pub(crate) fn new() -> Self { + pub(crate) fn new(global_compile_environment: Gc>) -> Self { Self { stack: vec![Gc::new(DeclarativeEnvironment { bindings: Cell::new(Vec::new()), this: None, + compile: global_compile_environment, + poisoned: Cell::new(false), })], } } + /// Extends the length of the next outer function environment to the number of compiled bindings. + /// + /// This is only useful when compiled bindings are added after the initial compilation (eval). + pub(crate) fn extend_outer_function_environment(&mut self) { + for env in self.stack.iter().rev() { + if env.this.is_some() { + let compile_bindings_number = env.compile.borrow().num_bindings(); + let mut bindings_mut = env.bindings.borrow_mut(); + + if compile_bindings_number > bindings_mut.len() { + let diff = compile_bindings_number - bindings_mut.len(); + bindings_mut.extend(vec![None; diff]); + } + break; + } + } + } + + /// Check if any of the provided binding names are defined as lexical bindings. + /// + /// Start at the current environment. + /// Stop at the next outer function environment. + pub(crate) fn has_lex_binding_until_function_environment( + &self, + names: &FxHashSet, + ) -> Option { + for env in self.stack.iter().rev() { + let compile = env.compile.borrow(); + for name in names { + if compile.has_lex_binding(*name) { + return Some(*name); + } + } + if compile.is_function() { + break; + } + } + None + } + + /// Pop all current environments except the global environment. + pub(crate) fn pop_to_global(&mut self) -> Vec> { + self.stack.split_off(1) + } + + /// Get the number of current environments. + pub(crate) fn len(&self) -> usize { + self.stack.len() + } + + /// Truncate current environments to the given number. + pub(crate) fn truncate(&mut self, len: usize) { + self.stack.truncate(len); + } + + /// Extend the current environment stack with the given environments. + pub(crate) fn extend(&mut self, other: Vec>) { + self.stack.extend(other); + } + /// Set the number of bindings on the global environment. /// /// # Panics @@ -97,20 +175,57 @@ impl DeclarativeEnvironmentStack { } /// Push a declarative environment on the environments stack. + /// + /// # Panics + /// + /// Panics if no environment exists on the stack. #[inline] - pub(crate) fn push_declarative(&mut self, num_bindings: usize) { + pub(crate) fn push_declarative( + &mut self, + num_bindings: usize, + compile_environment: Gc>, + ) { + let poisoned = self + .stack + .last() + .expect("global environment must always exist") + .poisoned + .borrow() + .to_owned(); + self.stack.push(Gc::new(DeclarativeEnvironment { bindings: Cell::new(vec![None; num_bindings]), this: None, + compile: compile_environment, + poisoned: Cell::new(poisoned), })); } /// Push a function environment on the environments stack. + /// + /// # Panics + /// + /// Panics if no environment exists on the stack. #[inline] - pub(crate) fn push_function(&mut self, num_bindings: usize, this: JsValue) { + pub(crate) fn push_function( + &mut self, + num_bindings: usize, + compile_environment: Gc>, + this: JsValue, + ) { + let poisoned = self + .stack + .last() + .expect("global environment must always exist") + .poisoned + .borrow() + .to_owned(); + self.stack.push(Gc::new(DeclarativeEnvironment { bindings: Cell::new(vec![None; num_bindings]), this: Some(this), + compile: compile_environment, + poisoned: Cell::new(poisoned), })); } @@ -134,6 +249,47 @@ impl DeclarativeEnvironmentStack { .clone() } + /// Get the compile environment for the current runtime environment. + /// + /// # Panics + /// + /// Panics if no environment exists on the stack. + pub(crate) fn current_compile_environment(&self) -> Gc> { + self.stack + .last() + .expect("global environment must always exist") + .compile + .clone() + } + + /// Mark that there may be added bindings in the current environment. + /// + /// # Panics + /// + /// Panics if no environment exists on the stack. + #[inline] + pub(crate) fn poison_current(&mut self) { + let mut poisoned = self + .stack + .last() + .expect("global environment must always exist") + .poisoned + .borrow_mut(); + *poisoned = true; + } + + /// Mark that there may be added binding in all environments. + #[inline] + pub(crate) fn poison_all(&mut self) { + for env in &mut self.stack { + let mut poisoned = env.poisoned.borrow_mut(); + if *poisoned { + return; + } + *poisoned = true; + } + } + /// Get the value of a binding. /// /// # Panics @@ -142,9 +298,30 @@ impl DeclarativeEnvironmentStack { #[inline] pub(crate) fn get_value_optional( &self, - environment_index: usize, - binding_index: usize, + mut environment_index: usize, + mut binding_index: usize, + name: Sym, ) -> Option { + if environment_index != self.stack.len() - 1 { + for env_index in (environment_index + 1..self.stack.len()).rev() { + let env = self + .stack + .get(env_index) + .expect("environment index must be in range"); + if !*env.poisoned.borrow() { + break; + } + let compile = env.compile.borrow(); + if compile.is_function() { + if let Some(b) = compile.get_binding(name) { + environment_index = b.environment_index; + binding_index = b.binding_index; + break; + } + } + } + } + self.stack .get(environment_index) .expect("environment index must be in range") @@ -155,6 +332,34 @@ impl DeclarativeEnvironmentStack { .clone() } + /// Get the value of a binding by it's name. + /// + /// This only considers function environments that are poisoned. + /// All other bindings are accessed via indices. + #[inline] + pub(crate) fn get_value_global_poisoned(&self, name: Sym) -> Option { + for env in self.stack.iter().rev() { + if !*env.poisoned.borrow() { + return None; + } + let compile = env.compile.borrow(); + if compile.is_function() { + if let Some(b) = compile.get_binding(name) { + return self + .stack + .get(b.environment_index) + .expect("environment index must be in range") + .bindings + .borrow() + .get(b.binding_index) + .expect("binding index must be in range") + .clone(); + } + } + } + None + } + /// Set the value of a binding. /// /// # Panics @@ -188,10 +393,31 @@ impl DeclarativeEnvironmentStack { #[inline] pub(crate) fn put_value_if_initialized( &mut self, - environment_index: usize, - binding_index: usize, + mut environment_index: usize, + mut binding_index: usize, + name: Sym, value: JsValue, ) -> bool { + if environment_index != self.stack.len() - 1 { + for env_index in (environment_index + 1..self.stack.len()).rev() { + let env = self + .stack + .get(env_index) + .expect("environment index must be in range"); + if !*env.poisoned.borrow() { + break; + } + let compile = env.compile.borrow(); + if compile.is_function() { + if let Some(b) = compile.get_binding(name) { + environment_index = b.environment_index; + binding_index = b.binding_index; + break; + } + } + } + } + let mut bindings = self .stack .get(environment_index) @@ -234,6 +460,40 @@ impl DeclarativeEnvironmentStack { *binding = Some(value); } } + + /// Set the value of a binding by it's name. + /// + /// This only considers function environments that are poisoned. + /// All other bindings are set via indices. + /// + /// # Panics + /// + /// Panics if the environment or binding index are out of range. + #[inline] + pub(crate) fn put_value_global_poisoned(&mut self, name: Sym, value: &JsValue) -> bool { + for env in self.stack.iter().rev() { + if !*env.poisoned.borrow() { + return false; + } + let compile = env.compile.borrow(); + if compile.is_function() { + if let Some(b) = compile.get_binding(name) { + let mut bindings = self + .stack + .get(b.environment_index) + .expect("environment index must be in range") + .bindings + .borrow_mut(); + let binding = bindings + .get_mut(b.binding_index) + .expect("binding index must be in range"); + *binding = Some(value.clone()); + return true; + } + } + } + false + } } /// A binding locator contains all information about a binding that is needed to resolve it at runtime. diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index 1e9c5ab6cd..66f50e00f1 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -5,9 +5,10 @@ //! A realm is represented in this implementation as a Realm struct with the fields specified from the spec. use crate::{ - environments::{CompileTimeEnvironmentStack, DeclarativeEnvironmentStack}, + environments::{CompileTimeEnvironment, DeclarativeEnvironmentStack}, object::{GlobalPropertyMap, JsObject, ObjectData, PropertyMap}, }; +use boa_gc::{Cell, Gc}; use boa_profiler::Profiler; /// Representation of a Realm. @@ -19,7 +20,7 @@ pub struct Realm { pub(crate) global_extensible: bool, pub(crate) global_property_map: PropertyMap, pub(crate) environments: DeclarativeEnvironmentStack, - pub(crate) compile_env: CompileTimeEnvironmentStack, + pub(crate) compile_env: Gc>, } impl Realm { @@ -31,12 +32,14 @@ impl Realm { // Allow identification of the global object easily let global_object = JsObject::from_proto_and_data(None, ObjectData::global()); + let global_compile_environment = Gc::new(Cell::new(CompileTimeEnvironment::new_global())); + Self { global_object, global_extensible: true, global_property_map: PropertyMap::default(), - environments: DeclarativeEnvironmentStack::new(), - compile_env: CompileTimeEnvironmentStack::new(), + environments: DeclarativeEnvironmentStack::new(global_compile_environment.clone()), + compile_env: global_compile_environment, } } @@ -53,7 +56,7 @@ impl Realm { /// 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(); + let binding_number = self.compile_env.borrow().num_bindings(); self.environments.set_global_binding_number(binding_number); } } diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index fc6574a069..80e3c34f10 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -11,7 +11,7 @@ use crate::{ generator::{Generator, GeneratorContext, GeneratorState}, }, context::intrinsics::StandardConstructors, - environments::{BindingLocator, DeclarativeEnvironmentStack}, + environments::{BindingLocator, CompileTimeEnvironment, DeclarativeEnvironmentStack}, object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData}, property::{PropertyDescriptor, PropertyKey}, syntax::ast::node::FormalParameterList, @@ -100,6 +100,9 @@ pub struct CodeBlock { /// Similar to the `[[ClassFieldInitializerName]]` slot in the spec. /// Holds class field names that are computed at class declaration time. pub(crate) computed_field_names: Option>>, + + /// Compile time environments in this function. + pub(crate) compile_environments: Vec>>, } impl CodeBlock { @@ -121,6 +124,7 @@ impl CodeBlock { lexical_name_argument: false, arguments_binding: None, computed_field_names: None, + compile_environments: Vec::new(), } } @@ -190,6 +194,8 @@ impl CodeBlock { | Opcode::LogicalAnd | Opcode::LogicalOr | Opcode::Coalesce + | Opcode::CallEval + | Opcode::CallEvalWithRest | Opcode::Call | Opcode::CallWithRest | Opcode::New @@ -198,13 +204,14 @@ impl CodeBlock { | Opcode::ForInLoopNext | Opcode::ConcatToString | Opcode::CopyDataProperties - | Opcode::GeneratorNextDelegate - | Opcode::PushDeclarativeEnvironment => { + | Opcode::GeneratorNextDelegate => { let result = self.read::(*pc).to_string(); *pc += size_of::(); result } - Opcode::TryStart => { + Opcode::TryStart + | Opcode::PushDeclarativeEnvironment + | Opcode::PushFunctionEnvironment => { let operand1 = self.read::(*pc); *pc += size_of::(); let operand2 = self.read::(*pc); @@ -322,7 +329,6 @@ impl CodeBlock { | Opcode::FinallyEnd | Opcode::This | Opcode::Return - | Opcode::PushFunctionEnvironment | Opcode::PopEnvironment | Opcode::LoopStart | Opcode::LoopContinue @@ -634,10 +640,19 @@ impl JsObject { this.clone() }; - context - .realm - .environments - .push_function(code.num_bindings, this.clone()); + if code.params.has_expressions() { + context.realm.environments.push_function( + code.num_bindings, + code.compile_environments[1].clone(), + this.clone(), + ); + } else { + context.realm.environments.push_function( + code.num_bindings, + code.compile_environments[0].clone(), + this.clone(), + ); + } if let Some(binding) = code.arguments_binding { let arguments_obj = @@ -733,10 +748,19 @@ impl JsObject { this.clone() }; - context - .realm - .environments - .push_function(code.num_bindings, this.clone()); + if code.params.has_expressions() { + context.realm.environments.push_function( + code.num_bindings, + code.compile_environments[1].clone(), + this.clone(), + ); + } else { + context.realm.environments.push_function( + code.num_bindings, + code.compile_environments[0].clone(), + this.clone(), + ); + } if let Some(binding) = code.arguments_binding { let arguments_obj = @@ -904,10 +928,19 @@ impl JsObject { this }; - context - .realm - .environments - .push_function(code.num_bindings, this.clone().into()); + if code.params.has_expressions() { + context.realm.environments.push_function( + code.num_bindings, + code.compile_environments[1].clone(), + this.clone().into(), + ); + } else { + context.realm.environments.push_function( + code.num_bindings, + code.compile_environments[0].clone(), + this.clone().into(), + ); + } let mut arguments_in_parameter_names = false; let mut is_simple_parameter_list = true; diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 5546808960..62e09cc175 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -445,30 +445,42 @@ impl Context { 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(), &[])? - } + if let Some(value) = self + .realm + .environments + .get_value_global_poisoned(binding_locator.name()) + { + value + } else { + 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")) } - }, - _ => 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(), + binding_locator.name(), ) { value } else { @@ -484,28 +496,37 @@ impl Context { 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(), &[])? - } + if let Some(value) = self + .realm + .environments + .get_value_global_poisoned(binding_locator.name()) + { + value + } else { + 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(), - }, - _ => JsValue::undefined(), + } } } else if let Some(value) = self.realm.environments.get_value_optional( binding_locator.environment_index(), binding_locator.binding_index(), + binding_locator.name(), ) { value } else { @@ -521,31 +542,40 @@ impl Context { 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!( - "assignment to undeclared variable {key}" - )); - } - - let success = crate::object::internal_methods::global::global_set_no_receiver( - &key.clone().into(), - value, - self, - )?; + if !self + .realm + .environments + .put_value_global_poisoned(binding_locator.name(), &value) + { + 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!( + "assignment to undeclared variable {key}" + )); + } - if !success && (self.strict() || self.vm.frame().code.strict) { - return self - .throw_type_error(format!("cannot set non-writable property: {key}",)); + 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(), + binding_locator.name(), value, ) { self.throw_reference_error(format!( @@ -1200,6 +1230,95 @@ impl Context { let function = create_generator_function_object(code, self); self.vm.push(function); } + Opcode::CallEval => { + if self.vm.stack_size_limit <= self.vm.stack.len() { + return self.throw_range_error("Maximum call stack size exceeded"); + } + let argument_count = self.vm.read::(); + let mut arguments = Vec::with_capacity(argument_count as usize); + for _ in 0..argument_count { + arguments.push(self.vm.pop()); + } + arguments.reverse(); + + let func = self.vm.pop(); + let mut this = self.vm.pop(); + + let object = match func { + JsValue::Object(ref object) if object.is_callable() => object.clone(), + _ => return self.throw_type_error("not a callable function"), + }; + + if this.is_null_or_undefined() { + this = self.global_object().clone().into(); + } + + // A native function with the name "eval" implies, that is this the built-in eval function. + let eval = matches!(object.borrow().as_function(), Some(Function::Native { .. })); + + let strict = self.strict() || self.vm.frame().code.strict; + + if eval { + if let Some(x) = arguments.get(0) { + let result = + crate::builtins::eval::Eval::perform_eval(x, true, strict, self)?; + self.vm.push(result); + } else { + self.vm.push(JsValue::Undefined); + } + } else { + let result = object.__call__(&this, &arguments, self)?; + self.vm.push(result); + } + } + Opcode::CallEvalWithRest => { + if self.vm.stack_size_limit <= self.vm.stack.len() { + return self.throw_range_error("Maximum call stack size exceeded"); + } + let argument_count = self.vm.read::(); + let rest_argument = self.vm.pop(); + let mut arguments = Vec::with_capacity(argument_count as usize); + for _ in 0..(argument_count - 1) { + arguments.push(self.vm.pop()); + } + arguments.reverse(); + let func = self.vm.pop(); + let mut this = self.vm.pop(); + + let iterator_record = rest_argument.get_iterator(self, None, None)?; + let mut rest_arguments = Vec::new(); + while let Some(next) = iterator_record.step(self)? { + rest_arguments.push(next.value(self)?); + } + arguments.append(&mut rest_arguments); + + let object = match func { + JsValue::Object(ref object) if object.is_callable() => object.clone(), + _ => return self.throw_type_error("not a callable function"), + }; + + if this.is_null_or_undefined() { + this = self.global_object().clone().into(); + } + + // A native function with the name "eval" implies, that is this the built-in eval function. + let eval = matches!(object.borrow().as_function(), Some(Function::Native { .. })); + + let strict = self.strict() || self.vm.frame().code.strict; + + if eval { + if let Some(x) = arguments.get(0) { + let result = + crate::builtins::eval::Eval::perform_eval(x, true, strict, self)?; + self.vm.push(result); + } else { + self.vm.push(JsValue::Undefined); + } + } else { + let result = object.__call__(&this, &arguments, self)?; + self.vm.push(result); + } + } Opcode::Call => { if self.vm.stack_size_limit <= self.vm.stack.len() { return self.throw_range_error("Maximum call stack size exceeded"); @@ -1340,14 +1459,23 @@ impl Context { } Opcode::PushDeclarativeEnvironment => { let num_bindings = self.vm.read::(); + let compile_environments_index = self.vm.read::(); + let compile_environment = self.vm.frame().code.compile_environments + [compile_environments_index as usize] + .clone(); self.realm .environments - .push_declarative(num_bindings as usize); + .push_declarative(num_bindings as usize, compile_environment); self.vm.frame_mut().loop_env_stack_inc(); self.vm.frame_mut().try_env_stack_inc(); } Opcode::PushFunctionEnvironment => { let num_bindings = self.vm.read::(); + let compile_environments_index = self.vm.read::(); + let compile_environment = self.vm.frame().code.compile_environments + [compile_environments_index as usize] + .clone(); + let is_constructor = self.vm.frame().code.constructor; let is_lexical = self.vm.frame().code.this_mode.is_lexical(); let this = if is_constructor || !is_lexical { @@ -1356,9 +1484,11 @@ impl Context { JsValue::undefined() }; - self.realm - .environments - .push_function(num_bindings as usize, this); + self.realm.environments.push_function( + num_bindings as usize, + compile_environment, + this, + ); } Opcode::PopEnvironment => { self.realm.environments.pop(); @@ -1839,7 +1969,7 @@ impl Context { println!("\n"); } - if self.vm.stack.is_empty() { + if self.vm.stack.len() <= start_stack_size { return Ok((JsValue::undefined(), ReturnType::Normal)); } diff --git a/boa_engine/src/vm/opcode.rs b/boa_engine/src/vm/opcode.rs index 4bce1bc343..7523c2e9d1 100644 --- a/boa_engine/src/vm/opcode.rs +++ b/boa_engine/src/vm/opcode.rs @@ -825,6 +825,20 @@ pub enum Opcode { /// Stack: **=>** func GetGenerator, + /// Call a function named "eval". + /// + /// Operands: argument_count: `u32` + /// + /// Stack: func, this, argument_1, ... argument_n **=>** result + CallEval, + + /// Call a function named "eval" where the last argument is a rest parameter. + /// + /// Operands: argument_count: `u32` + /// + /// Stack: func, this, argument_1, ... argument_n **=>** result + CallEvalWithRest, + /// Call a function. /// /// Operands: argument_count: `u32` @@ -862,14 +876,14 @@ pub enum Opcode { /// Push a declarative environment. /// - /// Operands: num_bindings: `u32` + /// Operands: num_bindings: `u32`, compile_environments_index: `u32` /// /// Stack: **=>** PushDeclarativeEnvironment, /// Push a function environment. /// - /// Operands: + /// Operands: num_bindings: `u32`, compile_environments_index: `u32` /// /// Stack: **=>** PushFunctionEnvironment, @@ -1155,6 +1169,8 @@ impl Opcode { Opcode::Default => "Default", Opcode::GetFunction => "GetFunction", Opcode::GetGenerator => "GetGenerator", + Opcode::CallEval => "CallEval", + Opcode::CallEvalWithRest => "CallEvalWithRest", Opcode::Call => "Call", Opcode::CallWithRest => "CallWithRest", Opcode::New => "New", @@ -1287,6 +1303,8 @@ impl Opcode { Opcode::Default => "INST - Default", Opcode::GetFunction => "INST - GetFunction", Opcode::GetGenerator => "INST - GetGenerator", + Opcode::CallEval => "INST - CallEval", + Opcode::CallEvalWithRest => "INST - CallEvalWithRest", Opcode::Call => "INST - Call", Opcode::CallWithRest => "INST - CallWithRest", Opcode::New => "INST - New",