diff --git a/.github/workflows/test262.yml b/.github/workflows/test262.yml index fdce366efb9..d4efb939ea1 100644 --- a/.github/workflows/test262.yml +++ b/.github/workflows/test262.yml @@ -46,7 +46,7 @@ jobs: run: | cd boa mkdir ../results - cargo run --release --bin boa_tester -- run -v -o ../results/test262 + cargo run --release --bin boa_tester -- run -O -v -o ../results/test262 cd .. # Run the results comparison diff --git a/boa_ast/src/expression/literal/array.rs b/boa_ast/src/expression/literal/array.rs index ee0291f1386..21f99fa195a 100644 --- a/boa_ast/src/expression/literal/array.rs +++ b/boa_ast/src/expression/literal/array.rs @@ -159,6 +159,13 @@ impl AsRef<[Option]> for ArrayLiteral { } } +impl AsMut<[Option]> for ArrayLiteral { + #[inline] + fn as_mut(&mut self) -> &mut [Option] { + &mut self.arr + } +} + impl From for ArrayLiteral where T: Into]>>, diff --git a/boa_ast/src/expression/literal/mod.rs b/boa_ast/src/expression/literal/mod.rs index 6c1b67e8b44..9694af4eca5 100644 --- a/boa_ast/src/expression/literal/mod.rs +++ b/boa_ast/src/expression/literal/mod.rs @@ -110,6 +110,12 @@ pub enum Literal { /// [spec]: https://tc39.es/ecma262/#sec-null-value /// [mdn]: https://developer.mozilla.org/en-US/docs/Glossary/null Null, + + /// This represents the JavaScript `undefined` value, it does not reference the `undefined` global variable, + /// it will directly evaluate to `undefined`. + /// + /// NOTE: This is used for optimizations. + Undefined, } impl From for Literal { @@ -173,6 +179,7 @@ impl ToInternedString for Literal { Self::BigInt(ref num) => num.to_string(), Self::Bool(v) => v.to_string(), Self::Null => "null".to_owned(), + Self::Undefined => "undefined".to_owned(), } } } diff --git a/boa_ast/src/expression/operator/binary/mod.rs b/boa_ast/src/expression/operator/binary/mod.rs index 8c1ddb63e93..8f52c30bf54 100644 --- a/boa_ast/src/expression/operator/binary/mod.rs +++ b/boa_ast/src/expression/operator/binary/mod.rs @@ -71,6 +71,20 @@ impl Binary { pub const fn rhs(&self) -> &Expression { &self.rhs } + + /// Gets the left hand side of the binary operation. + #[inline] + #[must_use] + pub fn lhs_mut(&mut self) -> &mut Expression { + &mut self.lhs + } + + /// Gets the right hand side of the binary operation. + #[inline] + #[must_use] + pub fn rhs_mut(&mut self) -> &mut Expression { + &mut self.rhs + } } impl ToInternedString for Binary { diff --git a/boa_ast/src/expression/operator/unary/mod.rs b/boa_ast/src/expression/operator/unary/mod.rs index 81c57fb96ac..f3e8cb244dc 100644 --- a/boa_ast/src/expression/operator/unary/mod.rs +++ b/boa_ast/src/expression/operator/unary/mod.rs @@ -60,6 +60,13 @@ impl Unary { pub fn target(&self) -> &Expression { self.target.as_ref() } + + /// Gets the target of this unary operator. + #[inline] + #[must_use] + pub fn target_mut(&mut self) -> &mut Expression { + self.target.as_mut() + } } impl ToInternedString for Unary { diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 98d759f0d89..640216545bc 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -64,6 +64,7 @@ use boa_ast::StatementList; use boa_engine::{ context::ContextBuilder, job::{FutureJob, JobQueue, NativeJob}, + optimizer::OptimizerOptions, vm::flowgraph::{Direction, Graph}, Context, JsResult, Source, }; @@ -89,6 +90,7 @@ const READLINE_COLOR: Color = Color::Cyan; // https://docs.rs/structopt/0.3.11/structopt/#type-magic #[derive(Debug, Parser)] #[command(author, version, about, name = "boa")] +#[allow(clippy::struct_excessive_bools)] // NOTE: Allow having more than 3 bools in struct struct Opt { /// The JavaScript file(s) to be evaluated. #[arg(name = "FILE", value_hint = ValueHint::FilePath)] @@ -118,6 +120,12 @@ struct Opt { #[arg(long = "vi")] vi_mode: bool, + #[arg(long, short = 'O', group = "optimizer")] + optimize: bool, + + #[arg(long, requires = "optimizer")] + optimizer_statistics: bool, + /// Generate instruction flowgraph. Default is Graphviz. #[arg( long, @@ -207,7 +215,11 @@ where S: AsRef<[u8]> + ?Sized, { if let Some(ref arg) = args.dump_ast { - let ast = parse_tokens(src, context)?; + let mut ast = parse_tokens(src, context)?; + + if args.optimize { + context.optimize_statement_list(&mut ast); + } match arg { Some(DumpFormat::Json) => println!( @@ -251,31 +263,17 @@ fn generate_flowgraph( Ok(result) } -fn main() -> Result<(), io::Error> { - let args = Opt::parse(); - - let queue = Jobs::default(); - let mut context = ContextBuilder::new() - .job_queue(&queue) - .build() - .expect("cannot fail with default global object"); - - // Strict mode - context.strict(args.strict); - - // Trace Output - context.set_trace(args.trace); - +fn evaluate_files(args: &Opt, context: &mut Context<'_>) -> Result<(), io::Error> { for file in &args.files { let buffer = read(file)?; if args.has_dump_flag() { - if let Err(e) = dump(&buffer, &args, &mut context) { + if let Err(e) = dump(&buffer, args, context) { eprintln!("{e}"); } } else if let Some(flowgraph) = args.flowgraph { match generate_flowgraph( - &mut context, + context, &buffer, flowgraph.unwrap_or(FlowgraphFormat::Graphviz), args.flowgraph_direction, @@ -292,6 +290,30 @@ fn main() -> Result<(), io::Error> { } } + Ok(()) +} + +fn main() -> Result<(), io::Error> { + let args = Opt::parse(); + + let queue = Jobs::default(); + let mut context = ContextBuilder::new() + .job_queue(&queue) + .build() + .expect("cannot fail with default global object"); + + // Strict mode + context.strict(args.strict); + + // Trace Output + context.set_trace(args.trace); + + // Configure optimizer options + let mut optimizer_options = OptimizerOptions::empty(); + optimizer_options.set(OptimizerOptions::STATISTICS, args.optimizer_statistics); + optimizer_options.set(OptimizerOptions::OPTIMIZE_ALL, args.optimize); + context.set_optimizer_options(optimizer_options); + if args.files.is_empty() { let config = Config::builder() .keyseq_timeout(1) @@ -365,6 +387,8 @@ fn main() -> Result<(), io::Error> { editor .save_history(CLI_HISTORY) .expect("could not save CLI history"); + } else { + evaluate_files(&args, &mut context)?; } Ok(()) diff --git a/boa_engine/src/bytecompiler/expression/mod.rs b/boa_engine/src/bytecompiler/expression/mod.rs index d00752a763f..e52ababcac4 100644 --- a/boa_engine/src/bytecompiler/expression/mod.rs +++ b/boa_engine/src/bytecompiler/expression/mod.rs @@ -33,6 +33,7 @@ impl ByteCompiler<'_, '_> { AstLiteral::Bool(true) => self.emit(Opcode::PushTrue, &[]), AstLiteral::Bool(false) => self.emit(Opcode::PushFalse, &[]), AstLiteral::Null => self.emit(Opcode::PushNull, &[]), + AstLiteral::Undefined => self.emit(Opcode::PushUndefined, &[]), } if !use_expr { diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 7b8ae96289d..d5ed91b2dc0 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -498,8 +498,7 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { } // Check if the f64 value can fit in an i32. - #[allow(clippy::float_cmp)] - if f64::from(value as i32) == value { + if f64::from(value as i32).to_bits() == value.to_bits() { self.emit_push_integer(value as i32); } else { self.emit_opcode(Opcode::PushRational); diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 0e0052b63d1..0af71be6283 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -23,6 +23,7 @@ use crate::{ job::{IdleJobQueue, JobQueue, NativeJob}, native_function::NativeFunction, object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject}, + optimizer::{Optimizer, OptimizerOptions, OptimizerStatistics}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, vm::{CallFrame, CodeBlock, Vm}, @@ -101,6 +102,8 @@ pub struct Context<'host> { host_hooks: &'host dyn HostHooks, job_queue: &'host dyn JobQueue, + + optimizer_options: OptimizerOptions, } impl std::fmt::Debug for Context<'_> { @@ -113,7 +116,8 @@ impl std::fmt::Debug for Context<'_> { .field("vm", &self.vm) .field("strict", &self.strict) .field("promise_job_queue", &"JobQueue") - .field("hooks", &"HostHooks"); + .field("hooks", &"HostHooks") + .field("optimize", &self.optimizer_options); #[cfg(feature = "intl")] debug.field("icu", &self.icu); @@ -202,6 +206,15 @@ impl Context<'_> { result } + /// Applies optimizations to the [`StatementList`] inplace. + pub fn optimize_statement_list( + &mut self, + statement_list: &mut StatementList, + ) -> OptimizerStatistics { + let mut optimizer = Optimizer::new(self); + optimizer.apply(statement_list) + } + /// Parse the given source script. pub fn parse_script( &mut self, @@ -212,7 +225,11 @@ impl Context<'_> { if self.strict { parser.set_strict(); } - parser.parse_script(&mut self.interner) + let mut result = parser.parse_script(&mut self.interner)?; + if !self.optimizer_options().is_empty() { + self.optimize_statement_list(&mut result); + } + Ok(result) } /// Parse the given source script. @@ -426,6 +443,15 @@ impl Context<'_> { self.vm.trace = trace; } + /// Get optimizer options. + pub const fn optimizer_options(&self) -> OptimizerOptions { + self.optimizer_options + } + /// Enable or disable optimizations + pub fn set_optimizer_options(&mut self, optimizer_options: OptimizerOptions) { + self.optimizer_options = optimizer_options; + } + /// Changes the strictness mode of the context. pub fn strict(&mut self, strict: bool) { self.strict = strict; @@ -642,6 +668,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { kept_alive: Vec::new(), host_hooks, job_queue: self.job_queue.unwrap_or(&IdleJobQueue), + optimizer_options: OptimizerOptions::empty(), }; builtins::set_default_global_bindings(&mut context)?; diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 023968e80d9..9e27be29459 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -124,6 +124,8 @@ pub mod symbol; pub mod value; pub mod vm; +pub mod optimizer; + #[cfg(feature = "console")] pub mod console; diff --git a/boa_engine/src/optimizer/mod.rs b/boa_engine/src/optimizer/mod.rs new file mode 100644 index 00000000000..9c8a6915c57 --- /dev/null +++ b/boa_engine/src/optimizer/mod.rs @@ -0,0 +1,135 @@ +//! Implements optimizations. + +pub(crate) mod pass; +pub(crate) mod walker; + +use self::{pass::ConstantFolding, walker::Walker}; +use crate::Context; +use bitflags::bitflags; +use boa_ast::{visitor::VisitorMut, Expression, StatementList}; +use std::{fmt, ops::ControlFlow}; + +bitflags! { + /// Optimizer options. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct OptimizerOptions: u8 { + /// Print statistics to `stdout`. + const STATISTICS = 0b0000_0001; + + /// Apply contant folding optimization. + const CONSTANT_FOLDING = 0b0000_0010; + + /// Apply all optimizations. + const OPTIMIZE_ALL = Self::CONSTANT_FOLDING.bits(); + } +} + +/// The action to be performed after an optimization step. +#[derive(Debug)] +pub(crate) enum PassAction { + /// Keep the node, do nothing. + Keep, + + /// The node was modified inplace. + Modified, + + /// Replace the node. + Replace(T), +} + +/// Contains statistics about the optimizer execution. +#[derive(Debug, Default, Clone, Copy)] +pub struct OptimizerStatistics { + /// How many times was the optimization run in total. + pub constant_folding_run_count: usize, + + /// How many passes did the optimization run in total. + pub constant_folding_pass_count: usize, +} + +impl fmt::Display for OptimizerStatistics { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Optimizer {{")?; + writeln!( + f, + " constant folding: {} run(s), {} pass(es) ({} mutating, {} checking)", + self.constant_folding_run_count, + self.constant_folding_pass_count, + self.constant_folding_pass_count + .saturating_sub(self.constant_folding_run_count), + self.constant_folding_run_count + )?; + writeln!(f, "}}")?; + Ok(()) + } +} + +/// This represents an AST optimizer. +#[derive(Debug)] +pub(crate) struct Optimizer<'context, 'host> { + statistics: OptimizerStatistics, + context: &'context mut Context<'host>, +} + +impl<'context, 'host> Optimizer<'context, 'host> { + /// Create a optimizer. + pub(crate) fn new(context: &'context mut Context<'host>) -> Self { + Self { + statistics: OptimizerStatistics::default(), + context, + } + } + + /// Run the constant folding optimization on an expression. + fn run_constant_folding_pass(&mut self, expr: &mut Expression) -> bool { + self.statistics.constant_folding_run_count += 1; + + let mut has_changes = false; + loop { + self.statistics.constant_folding_pass_count += 1; + let mut walker = Walker::new(|expr| -> PassAction { + ConstantFolding::fold_expression(expr, self.context) + }); + // NOTE: postoder traversal is optimal for constant folding, + // since it evaluates the tree bottom-up. + walker.walk_expression_postorder(expr); + if !walker.changed() { + break; + } + has_changes = true; + } + has_changes + } + + fn run_all(&mut self, expr: &mut Expression) { + if self + .context + .optimizer_options() + .contains(OptimizerOptions::CONSTANT_FOLDING) + { + self.run_constant_folding_pass(expr); + } + } + + /// Apply optimizations inplace. + pub(crate) fn apply(&mut self, statement_list: &mut StatementList) -> OptimizerStatistics { + self.visit_statement_list_mut(statement_list); + if self + .context + .optimizer_options() + .contains(OptimizerOptions::STATISTICS) + { + println!("{}", self.statistics); + } + self.statistics + } +} + +impl<'ast> VisitorMut<'ast> for Optimizer<'_, '_> { + type BreakTy = (); + + fn visit_expression_mut(&mut self, node: &'ast mut Expression) -> ControlFlow { + self.run_all(node); + ControlFlow::Continue(()) + } +} diff --git a/boa_engine/src/optimizer/pass/constant_folding.rs b/boa_engine/src/optimizer/pass/constant_folding.rs new file mode 100644 index 00000000000..4e26a0772d5 --- /dev/null +++ b/boa_engine/src/optimizer/pass/constant_folding.rs @@ -0,0 +1,229 @@ +use crate::{ + builtins::Number, optimizer::PassAction, value::Numeric, Context, JsBigInt, JsString, JsValue, +}; +use boa_ast::{ + expression::{ + literal::Literal, + operator::{ + binary::{ArithmeticOp, BinaryOp, BitwiseOp, LogicalOp, RelationalOp}, + unary::UnaryOp, + Binary, Unary, + }, + }, + Expression, +}; + +fn literal_to_js_value(literal: &Literal, context: &mut Context<'_>) -> JsValue { + match literal { + Literal::String(v) => JsValue::new(JsString::from( + context.interner().resolve_expect(*v).utf16(), + )), + Literal::Num(v) => JsValue::new(*v), + Literal::Int(v) => JsValue::new(*v), + Literal::BigInt(v) => JsValue::new(JsBigInt::new(v.clone())), + Literal::Bool(v) => JsValue::new(*v), + Literal::Null => JsValue::null(), + Literal::Undefined => JsValue::undefined(), + } +} + +fn js_value_to_literal(value: JsValue, context: &mut Context<'_>) -> Literal { + match value { + JsValue::Null => Literal::Null, + JsValue::Undefined => Literal::Undefined, + JsValue::Boolean(v) => Literal::Bool(v), + JsValue::String(v) => Literal::String(context.interner_mut().get_or_intern(v.as_ref())), + JsValue::Rational(v) => Literal::Num(v), + JsValue::Integer(v) => Literal::Int(v), + JsValue::BigInt(v) => Literal::BigInt(Box::new(v.as_inner().clone())), + JsValue::Object(_) | JsValue::Symbol(_) => { + unreachable!("value must not be a object or symbol") + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct ConstantFolding {} + +impl ConstantFolding { + pub(crate) fn fold_expression( + expr: &mut Expression, + context: &mut Context<'_>, + ) -> PassAction { + match expr { + Expression::Unary(unary) => Self::constant_fold_unary_expr(unary, context), + Expression::Binary(binary) => Self::constant_fold_binary_expr(binary, context), + _ => PassAction::Keep, + } + } + + fn constant_fold_unary_expr( + unary: &mut Unary, + context: &mut Context<'_>, + ) -> PassAction { + let Expression::Literal(literal) = unary.target() else { + return PassAction::Keep; + }; + let value = match (literal, unary.op()) { + (literal, UnaryOp::Minus) => literal_to_js_value(literal, context).neg(context), + (literal, UnaryOp::Plus) => literal_to_js_value(literal, context) + .to_number(context) + .map(JsValue::new), + (literal, UnaryOp::Not) => literal_to_js_value(literal, context) + .not() + .map(JsValue::new), + (literal, UnaryOp::Tilde) => Ok( + match literal_to_js_value(literal, context) + .to_numeric(context) + .expect("should not fail") + { + Numeric::Number(number) => Number::not(number).into(), + Numeric::BigInt(bigint) => JsBigInt::not(&bigint).into(), + }, + ), + (literal, UnaryOp::TypeOf) => Ok(JsValue::new( + literal_to_js_value(literal, context).type_of(), + )), + (_, UnaryOp::Delete) => { + return PassAction::Replace(Expression::Literal(Literal::Bool(true))) + } + (_, UnaryOp::Void) => { + return PassAction::Replace(Expression::Literal(Literal::Undefined)) + } + }; + + // If it fails then revert changes + let Ok(value) = value else { + return PassAction::Keep; + }; + + PassAction::Replace(Expression::Literal(js_value_to_literal(value, context))) + } + + fn constant_fold_binary_expr( + binary: &mut Binary, + context: &mut Context<'_>, + ) -> PassAction { + let Expression::Literal(lhs) = binary.lhs() else { + return PassAction::Keep; + }; + + // We know that the lhs is a literal (pure expression) therefore the following + // optimization can be done: + // + // (pure_expression, call()) --> call() + // + // We cannot optimize it if rhs is `eval` or function call, because it is considered an indirect call, + // which is not the same as direct call. + // + // The lhs will replace with `undefined`, to simplify it as much as possible: + // + // (complex_pure_expression, eval) --> (undefined, eval) + // (complex_pure_expression, Object.prototype.valueOf) --> (undefined, Object.prototype.valueOf) + if binary.op() == BinaryOp::Comma { + if !matches!(binary.rhs(), Expression::Literal(_)) { + // If left-hand side is already undefined then just keep it, + // so we don't cause an infinite loop. + if *binary.lhs() == Expression::Literal(Literal::Undefined) { + return PassAction::Keep; + } + + *binary.lhs_mut() = Expression::Literal(Literal::Undefined); + return PassAction::Modified; + } + + // We take rhs, by replacing with a dummy value. + let rhs = std::mem::replace(binary.rhs_mut(), Expression::Literal(Literal::Undefined)); + return PassAction::Replace(rhs); + } + + let lhs = literal_to_js_value(lhs, context); + + // Do the following optimizations if it's a logical binary expression: + // + // falsy && call() --> falsy + // truthy || call() --> truthy + // null/undefined ?? call() --> call() + // + // The following **only** apply if the left-hand side is a pure expression (without side-effects): + // + // NOTE: The left-hand side is always pure because we check that it is a literal, above. + // + // falsy || call() --> call() + // truthy && call() --> call() + // non-null/undefined ?? call() --> non-null/undefined + if let BinaryOp::Logical(op) = binary.op() { + let expr = match op { + LogicalOp::And => { + if lhs.to_boolean() { + std::mem::replace(binary.rhs_mut(), Expression::Literal(Literal::Undefined)) + } else { + std::mem::replace(binary.lhs_mut(), Expression::Literal(Literal::Undefined)) + } + } + LogicalOp::Or => { + if lhs.to_boolean() { + std::mem::replace(binary.lhs_mut(), Expression::Literal(Literal::Undefined)) + } else { + std::mem::replace(binary.rhs_mut(), Expression::Literal(Literal::Undefined)) + } + } + LogicalOp::Coalesce => { + if lhs.is_null_or_undefined() { + std::mem::replace(binary.rhs_mut(), Expression::Literal(Literal::Undefined)) + } else { + std::mem::replace(binary.lhs_mut(), Expression::Literal(Literal::Undefined)) + } + } + }; + return PassAction::Replace(expr); + } + + let Expression::Literal(rhs) = binary.rhs() else { + return PassAction::Keep; + }; + + let rhs = literal_to_js_value(rhs, context); + + let value = match binary.op() { + BinaryOp::Arithmetic(op) => match op { + ArithmeticOp::Add => lhs.add(&rhs, context), + ArithmeticOp::Sub => lhs.sub(&rhs, context), + ArithmeticOp::Div => lhs.div(&rhs, context), + ArithmeticOp::Mul => lhs.mul(&rhs, context), + ArithmeticOp::Exp => lhs.pow(&rhs, context), + ArithmeticOp::Mod => lhs.rem(&rhs, context), + }, + BinaryOp::Bitwise(op) => match op { + BitwiseOp::And => lhs.bitand(&rhs, context), + BitwiseOp::Or => lhs.bitor(&rhs, context), + BitwiseOp::Xor => lhs.bitxor(&rhs, context), + BitwiseOp::Shl => lhs.shl(&rhs, context), + BitwiseOp::Shr => lhs.shr(&rhs, context), + BitwiseOp::UShr => lhs.ushr(&rhs, context), + }, + BinaryOp::Relational(op) => match op { + RelationalOp::In | RelationalOp::InstanceOf => return PassAction::Keep, + RelationalOp::Equal => lhs.equals(&rhs, context).map(JsValue::new), + RelationalOp::NotEqual => lhs.equals(&rhs, context).map(|x| !x).map(JsValue::new), + RelationalOp::StrictEqual => Ok(JsValue::new(lhs.strict_equals(&rhs))), + RelationalOp::StrictNotEqual => Ok(JsValue::new(!lhs.strict_equals(&rhs))), + RelationalOp::GreaterThan => lhs.gt(&rhs, context).map(JsValue::new), + RelationalOp::GreaterThanOrEqual => lhs.ge(&rhs, context).map(JsValue::new), + RelationalOp::LessThan => lhs.lt(&rhs, context).map(JsValue::new), + RelationalOp::LessThanOrEqual => lhs.le(&rhs, context).map(JsValue::new), + }, + BinaryOp::Logical(_) => { + unreachable!("We already checked if it's a logical binary expression!") + } + BinaryOp::Comma => unreachable!("We already checked if it's a comma expression!"), + }; + + // If it fails then revert changes + let Ok(value) = value else { + return PassAction::Keep; + }; + + PassAction::Replace(Expression::Literal(js_value_to_literal(value, context))) + } +} diff --git a/boa_engine/src/optimizer/pass/mod.rs b/boa_engine/src/optimizer/pass/mod.rs new file mode 100644 index 00000000000..166b1180ce7 --- /dev/null +++ b/boa_engine/src/optimizer/pass/mod.rs @@ -0,0 +1,3 @@ +mod constant_folding; + +pub(crate) use constant_folding::ConstantFolding; diff --git a/boa_engine/src/optimizer/walker.rs b/boa_engine/src/optimizer/walker.rs new file mode 100644 index 00000000000..d38a00b75b7 --- /dev/null +++ b/boa_engine/src/optimizer/walker.rs @@ -0,0 +1,59 @@ +use super::PassAction; +use boa_ast::{ + visitor::{VisitWith, VisitorMut}, + Expression, +}; +use std::{convert::Infallible, ops::ControlFlow}; + +/// The utility structure that traverses the AST. +pub(crate) struct Walker +where + F: FnMut(&mut Expression) -> PassAction, +{ + /// The function to be applied to the node. + f: F, + + /// Did a change happen while traversing. + changed: bool, +} + +impl Walker +where + F: FnMut(&mut Expression) -> PassAction, +{ + pub(crate) const fn new(f: F) -> Self { + Self { f, changed: false } + } + + pub(crate) const fn changed(&self) -> bool { + self.changed + } + + /// Walk the AST in postorder. + pub(crate) fn walk_expression_postorder(&mut self, expr: &mut Expression) { + self.visit_expression_mut(expr); + } +} + +impl<'ast, F> VisitorMut<'ast> for Walker +where + F: FnMut(&mut Expression) -> PassAction, +{ + type BreakTy = Infallible; + + /// Visits the tree in postorder. + fn visit_expression_mut(&mut self, expr: &'ast mut Expression) -> ControlFlow { + expr.visit_with_mut(self); + + match (self.f)(expr) { + PassAction::Keep => {} + PassAction::Modified => self.changed = true, + PassAction::Replace(new) => { + *expr = new; + self.changed = true; + } + } + + ControlFlow::Continue(()) + } +} diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 38d285b64f9..bdd3975841a 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -8,8 +8,8 @@ use crate::{ }; use boa_engine::{ context::ContextBuilder, job::SimpleJobQueue, native_function::NativeFunction, - object::FunctionObjectBuilder, property::Attribute, Context, JsArgs, JsNativeErrorKind, - JsValue, Source, + object::FunctionObjectBuilder, optimizer::OptimizerOptions, property::Attribute, Context, + JsArgs, JsNativeErrorKind, JsValue, Source, }; use colored::Colorize; use rayon::prelude::*; @@ -17,7 +17,13 @@ use std::{cell::RefCell, rc::Rc}; impl TestSuite { /// Runs the test suite. - pub(crate) fn run(&self, harness: &Harness, verbose: u8, parallel: bool) -> SuiteResult { + pub(crate) fn run( + &self, + harness: &Harness, + verbose: u8, + parallel: bool, + optimizer_options: OptimizerOptions, + ) -> SuiteResult { if verbose != 0 { println!("Suite {}:", self.path.display()); } @@ -25,24 +31,24 @@ impl TestSuite { let suites: Vec<_> = if parallel { self.suites .par_iter() - .map(|suite| suite.run(harness, verbose, parallel)) + .map(|suite| suite.run(harness, verbose, parallel, optimizer_options)) .collect() } else { self.suites .iter() - .map(|suite| suite.run(harness, verbose, parallel)) + .map(|suite| suite.run(harness, verbose, parallel, optimizer_options)) .collect() }; let tests: Vec<_> = if parallel { self.tests .par_iter() - .flat_map(|test| test.run(harness, verbose)) + .flat_map(|test| test.run(harness, verbose, optimizer_options)) .collect() } else { self.tests .iter() - .flat_map(|test| test.run(harness, verbose)) + .flat_map(|test| test.run(harness, verbose, optimizer_options)) .collect() }; @@ -113,21 +119,32 @@ impl TestSuite { impl Test { /// Runs the test. - pub(crate) fn run(&self, harness: &Harness, verbose: u8) -> Vec { + pub(crate) fn run( + &self, + harness: &Harness, + verbose: u8, + optimizer_options: OptimizerOptions, + ) -> Vec { let mut results = Vec::new(); if self.flags.contains(TestFlags::STRICT) && !self.flags.contains(TestFlags::RAW) { - results.push(self.run_once(harness, true, verbose)); + results.push(self.run_once(harness, true, verbose, optimizer_options)); } if self.flags.contains(TestFlags::NO_STRICT) || self.flags.contains(TestFlags::RAW) { - results.push(self.run_once(harness, false, verbose)); + results.push(self.run_once(harness, false, verbose, optimizer_options)); } results } /// Runs the test once, in strict or non-strict mode - fn run_once(&self, harness: &Harness, strict: bool, verbose: u8) -> TestResult { + fn run_once( + &self, + harness: &Harness, + strict: bool, + verbose: u8, + optimizer_options: OptimizerOptions, + ) -> TestResult { let Ok(source) = Source::from_filepath(&self.path) else { if verbose > 1 { println!( @@ -185,6 +202,7 @@ impl Test { return (false, e); } context.strict(strict); + context.set_optimizer_options(optimizer_options); // TODO: timeout let value = match if self.is_module() { @@ -224,6 +242,8 @@ impl Test { let context = &mut Context::default(); context.strict(strict); + context.set_optimizer_options(OptimizerOptions::OPTIMIZE_ALL); + if self.is_module() { match context.parse_module(source) { Ok(module_item_list) => match context.compile_module(&module_item_list) { @@ -252,6 +272,8 @@ impl Test { } => { let context = &mut Context::default(); context.strict(strict); + context.set_optimizer_options(optimizer_options); + if let Err(e) = self.set_up_env(harness, context, AsyncResult::default()) { return (false, e); } diff --git a/boa_tester/src/main.rs b/boa_tester/src/main.rs index 4b9b30231b6..a79eafbee3c 100644 --- a/boa_tester/src/main.rs +++ b/boa_tester/src/main.rs @@ -75,6 +75,7 @@ use self::{ results::{compare_results, write_json}, }; use bitflags::bitflags; +use boa_engine::optimizer::OptimizerOptions; use clap::{ArgAction, Parser, ValueHint}; use color_eyre::{ eyre::{bail, WrapErr}, @@ -160,6 +161,10 @@ enum Cli { #[arg(short, long, default_value = "test", value_hint = ValueHint::AnyPath)] suite: PathBuf, + /// Enable optimizations + #[arg(long, short = 'O')] + optimize: bool, + /// Optional output folder for the full results information. #[arg(short, long, value_hint = ValueHint::DirPath)] output: Option, @@ -197,6 +202,7 @@ fn main() -> Result<()> { test262_path, suite, output, + optimize, disable_parallelism, ignored: ignore, } => run_test_suite( @@ -206,6 +212,11 @@ fn main() -> Result<()> { suite.as_path(), output.as_deref(), ignore.as_path(), + if optimize { + OptimizerOptions::OPTIMIZE_ALL + } else { + OptimizerOptions::empty() + }, ), Cli::Compare { base, @@ -223,6 +234,7 @@ fn run_test_suite( suite: &Path, output: Option<&Path>, ignored: &Path, + optimizer_options: OptimizerOptions, ) -> Result<()> { if let Some(path) = output { if path.exists() { @@ -256,7 +268,7 @@ fn run_test_suite( if verbose != 0 { println!("Test loaded, starting..."); } - test.run(&harness, verbose); + test.run(&harness, verbose, optimizer_options); println!(); } else { @@ -268,7 +280,7 @@ fn run_test_suite( if verbose != 0 { println!("Test suite loaded, starting tests..."); } - let results = suite.run(&harness, verbose, parallel); + let results = suite.run(&harness, verbose, parallel, optimizer_options); println!(); println!("Results:");