From 19664ea9727398a0aab93c22de09c75f4b909022 Mon Sep 17 00:00:00 2001 From: Haled Odat Date: Wed, 15 Mar 2023 17:24:34 +0100 Subject: [PATCH] Implement constant folding optmization --- boa_ast/src/expression/literal/array.rs | 7 + boa_ast/src/expression/literal/mod.rs | 11 ++ boa_ast/src/expression/operator/binary/mod.rs | 14 ++ boa_ast/src/expression/operator/unary/mod.rs | 7 + boa_cli/src/main.rs | 10 +- boa_engine/src/bytecompiler/expression/mod.rs | 1 + boa_engine/src/context/mod.rs | 27 +++- boa_engine/src/lib.rs | 2 + .../optimizer/constant_folding_optimizer.rs | 152 ++++++++++++++++++ boa_engine/src/optimizer/mod.rs | 124 ++++++++++++++ boa_tester/src/exec/mod.rs | 29 ++-- boa_tester/src/main.rs | 11 +- test262 | 2 +- 13 files changed, 382 insertions(+), 15 deletions(-) create mode 100644 boa_engine/src/optimizer/constant_folding_optimizer.rs create mode 100644 boa_engine/src/optimizer/mod.rs 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..ddbc04185d0 100644 --- a/boa_ast/src/expression/literal/mod.rs +++ b/boa_ast/src/expression/literal/mod.rs @@ -110,6 +110,16 @@ pub enum Literal { /// [spec]: https://tc39.es/ecma262/#sec-null-value /// [mdn]: https://developer.mozilla.org/en-US/docs/Glossary/null Null, + + /// TODO: doc + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-undefined-value + /// [mdn]: https://developer.mozilla.org/en-US/docs/Glossary/undefined + Undefined, } impl From for Literal { @@ -173,6 +183,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 bb5fb9bc914..801d1e39a83 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -114,6 +114,9 @@ struct Opt { #[arg(long = "vi")] vi_mode: bool, + #[arg(long, short = 'O')] + optimize: bool, + /// Generate instruction flowgraph. Default is Graphviz. #[arg( long, @@ -207,7 +210,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!( @@ -262,6 +269,7 @@ fn main() -> Result<(), io::Error> { // Trace Output context.set_trace(args.trace); + context.set_optimize(args.optimize); for file in &args.files { let buffer = read(file)?; 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/context/mod.rs b/boa_engine/src/context/mod.rs index e8695c17956..387f32e6543 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -23,12 +23,13 @@ use crate::{ job::{IdleJobQueue, JobQueue, NativeJob}, native_function::NativeFunction, object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject}, + optimizer::{constant_folding_optimizer::ConstantFoldingOptimizer, Optimizer}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, vm::{CallFrame, CodeBlock, Vm}, JsResult, JsValue, Source, }; -use boa_ast::{ModuleItemList, StatementList}; +use boa_ast::{visitor::VisitorMut, ModuleItemList, StatementList}; use boa_gc::Gc; use boa_interner::{Interner, Sym}; use boa_parser::{Error as ParseError, Parser}; @@ -101,6 +102,8 @@ pub struct Context<'host> { host_hooks: &'host dyn HostHooks, job_queue: &'host dyn JobQueue, + + optimize: bool, } impl std::fmt::Debug for Context<'_> { @@ -202,6 +205,15 @@ impl Context<'_> { result } + /// Applies optimizations to the [`StatementList`] inplace. + pub fn optimize_statement_list(&mut self, statement_list: &mut StatementList) { + let mut optimizer = Optimizer::new(&mut self.interner); + let mut cfo = ConstantFoldingOptimizer::default(); + optimizer.push_pass(&mut cfo); + + optimizer.visit_statement_list_mut(statement_list); + } + /// Parse the given source script. pub fn parse_script( &mut self, @@ -212,7 +224,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.optimize { + self.optimize_statement_list(&mut result); + } + Ok(result) } /// Parse the given source script. @@ -426,6 +442,11 @@ impl Context<'_> { self.vm.trace = trace; } + /// TODO: + pub fn set_optimize(&mut self, optimize: bool) { + self.optimize = optimize; + } + /// Changes the strictness mode of the context. pub fn strict(&mut self, strict: bool) { self.strict = strict; @@ -642,6 +663,8 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { kept_alive: Vec::new(), host_hooks, job_queue: self.job_queue.unwrap_or(&IdleJobQueue), + // TODO: maybe it should be off by default + optimize: true, }; builtins::set_default_global_bindings(&mut context)?; diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 023968e80d9..7eaf17259dc 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(crate) mod optimizer; + #[cfg(feature = "console")] pub mod console; diff --git a/boa_engine/src/optimizer/constant_folding_optimizer.rs b/boa_engine/src/optimizer/constant_folding_optimizer.rs new file mode 100644 index 00000000000..1166ea509d4 --- /dev/null +++ b/boa_engine/src/optimizer/constant_folding_optimizer.rs @@ -0,0 +1,152 @@ +use boa_ast::{ + expression::{ + literal::Literal, + operator::{ + binary::{ArithmeticOp, BinaryOp, BitwiseOp, LogicalOp, RelationalOp}, + unary::UnaryOp, + Binary, Unary, + }, + }, + Expression, +}; +use boa_interner::Interner; + +use super::{Pass, PassResult}; + +#[derive(Debug, Default)] +pub(crate) struct ConstantFoldingOptimizer {} + +impl ConstantFoldingOptimizer { + pub(crate) fn constant_fold_unary_expr( + unary: &mut Unary, + interner: &mut Interner, + ) -> PassResult { + let value = if let Expression::Literal(Literal::Int(integer)) = unary.target() { + *integer + } else { + return PassResult::Leave; + }; + let literal = match unary.op() { + UnaryOp::Minus => Literal::Int(-value), + UnaryOp::Plus => Literal::Int(value), + UnaryOp::Not => Literal::Bool(value == 0), + UnaryOp::Tilde => Literal::Int(!value), + UnaryOp::TypeOf => Literal::String(interner.get_or_intern("number")), + UnaryOp::Delete => Literal::Bool(true), + UnaryOp::Void => Literal::Undefined, + }; + + PassResult::Replace(Expression::Literal(literal)) + } + pub(crate) fn constant_fold_binary_expr( + binary: &mut Binary, + _interner: &mut Interner, + ) -> PassResult { + let lhs = if let Expression::Literal(Literal::Int(integer)) = binary.lhs() { + *integer + } else { + return PassResult::Leave; + }; + let rhs = if let Expression::Literal(Literal::Int(integer)) = binary.rhs() { + *integer + } else { + return PassResult::Leave; + }; + + let literal = match binary.op() { + BinaryOp::Arithmetic(op) => match op { + ArithmeticOp::Add => { + if let Some(result) = lhs.checked_add(rhs) { + Literal::Int(result) + } else { + Literal::Num(f64::from(lhs) + f64::from(rhs)) + } + } + ArithmeticOp::Sub => { + if let Some(result) = lhs.checked_sub(rhs) { + Literal::Int(result) + } else { + Literal::Num(f64::from(lhs) - f64::from(rhs)) + } + } + ArithmeticOp::Div => { + if let Some(result) = lhs.checked_div(rhs).filter(|div| rhs * div == lhs) { + Literal::Int(result) + } else { + Literal::Num(f64::from(lhs) / f64::from(rhs)) + } + } + ArithmeticOp::Mul => { + if let Some(result) = lhs.checked_mul(rhs) { + Literal::Int(result) + } else { + Literal::Num(f64::from(lhs) - f64::from(rhs)) + } + } + ArithmeticOp::Exp => { + if rhs.is_positive() { + if let Some(result) = lhs.checked_pow(rhs as u32) { + Literal::Int(result) + } else { + Literal::Num(f64::from(lhs).powf(f64::from(rhs))) + } + } else { + Literal::Num(f64::from(lhs).powf(f64::from(rhs))) + } + } + ArithmeticOp::Mod => { + if let Some(result) = lhs.checked_rem(rhs) { + Literal::Int(result) + } else { + Literal::Num(f64::from(lhs) % f64::from(rhs)) + } + } + }, + BinaryOp::Bitwise(op) => match op { + BitwiseOp::And => Literal::Int(lhs & rhs), + BitwiseOp::Or => Literal::Int(lhs | rhs), + BitwiseOp::Xor => Literal::Int(lhs ^ rhs), + BitwiseOp::Shl => Literal::Int(lhs.wrapping_shl(rhs as u32)), + BitwiseOp::Shr => Literal::Int(lhs.wrapping_shr(rhs as u32)), + BitwiseOp::UShr => { + let result = (lhs as u32).wrapping_shr(rhs as u32); + if let Ok(result) = result.try_into() { + Literal::Int(result) + } else { + Literal::Num(f64::from(result)) + } + } + }, + BinaryOp::Relational(op) => match op { + RelationalOp::Equal | RelationalOp::StrictEqual => Literal::Bool(lhs == rhs), + RelationalOp::NotEqual | RelationalOp::StrictNotEqual => Literal::Bool(lhs != rhs), + RelationalOp::GreaterThan => Literal::Bool(lhs > rhs), + RelationalOp::GreaterThanOrEqual => Literal::Bool(lhs >= rhs), + RelationalOp::LessThan => Literal::Bool(lhs < rhs), + RelationalOp::LessThanOrEqual => Literal::Bool(lhs <= rhs), + RelationalOp::In | RelationalOp::InstanceOf => return PassResult::Leave, + }, + BinaryOp::Logical(op) => match op { + LogicalOp::And => Literal::Int(if lhs == 0 { lhs } else { rhs }), + LogicalOp::Or => Literal::Int(if lhs == 0 { rhs } else { lhs }), + LogicalOp::Coalesce => Literal::Int(rhs), + }, + BinaryOp::Comma => return PassResult::Leave, + }; + PassResult::Replace(Expression::Literal(literal)) + } +} + +impl Pass for ConstantFoldingOptimizer { + fn pass_expression( + &mut self, + expr: &mut Expression, + interner: &mut Interner, + ) -> PassResult { + match expr { + Expression::Unary(unary) => Self::constant_fold_unary_expr(unary, interner), + Expression::Binary(binary) => Self::constant_fold_binary_expr(binary, interner), + _ => PassResult::Leave, + } + } +} diff --git a/boa_engine/src/optimizer/mod.rs b/boa_engine/src/optimizer/mod.rs new file mode 100644 index 00000000000..6885c993de2 --- /dev/null +++ b/boa_engine/src/optimizer/mod.rs @@ -0,0 +1,124 @@ +#![allow(dead_code)] + +pub(crate) mod constant_folding_optimizer; + +use std::{fmt::Debug, ops::ControlFlow}; + +use boa_ast::{visitor::VisitorMut, Expression}; +use boa_interner::Interner; + +#[derive(Debug)] + +pub(crate) enum PassResult { + Leave, + Modified, + Replace(T), +} + +pub(crate) trait Pass: Debug { + fn pass_expression( + &mut self, + _expr: &mut Expression, + _interner: &mut Interner, + ) -> PassResult { + PassResult::::Leave + } +} + +pub(crate) struct Walker { + changed: bool, +} + +impl Walker { + pub(crate) const fn new() -> Self { + Self { changed: false } + } + + fn walk_expression_postorder_impl(&mut self, expr: &mut Expression, f: &mut F) -> bool + where + F: FnMut(&mut Expression) -> bool, + { + match expr { + Expression::ArrayLiteral(v) => { + for element in v.as_mut().iter_mut().flatten() { + self.changed |= self.walk_expression_postorder_impl(element, f); + } + } + Expression::Binary(v) => { + self.changed |= self.walk_expression_postorder_impl(v.lhs_mut(), f); + self.changed |= self.walk_expression_postorder_impl(v.rhs_mut(), f); + } + Expression::Unary(v) => { + self.changed |= self.walk_expression_postorder_impl(v.target_mut(), f); + } + // TODO: implement other branches + _ => {} + } + + self.changed |= f(expr); + self.changed + } + + pub(crate) fn walk_expression_postorder(&mut self, expr: &mut Expression, mut f: F) -> bool + where + F: FnMut(&mut Expression) -> PassResult, + { + self.walk_expression_postorder_impl(expr, &mut |expr: &mut Expression| -> bool { + let mut changed = false; + match f(expr) { + PassResult::Leave => {} + PassResult::Modified => changed = true, + PassResult::Replace(new) => { + *expr = new; + changed = true; + } + } + changed + }) + } +} + +#[derive(Debug)] +pub(crate) struct Optimizer<'pass> { + passes: Vec<&'pass mut dyn Pass>, + interner: &'pass mut Interner, +} + +impl<'pass> Optimizer<'pass> { + pub(crate) fn new(interner: &'pass mut Interner) -> Self { + Self { + passes: Vec::new(), + interner, + } + } + pub(crate) fn push_pass(&mut self, pass: &'pass mut dyn Pass) { + self.passes.push(pass); + } + + fn single_run(&mut self, expr: &mut Expression) -> bool { + // TODO: maybe run one until there is not change, then continue with the next? + let mut walker = Walker::new(); + for pass in &mut self.passes { + walker.walk_expression_postorder(expr, |expr| -> PassResult { + pass.pass_expression(expr, self.interner) + }); + } + walker.changed + } + + fn run(&mut self, expr: &mut Expression) { + let mut changed = false; + while !changed { + changed |= self.single_run(expr); + } + } +} + +impl<'ast> VisitorMut<'ast> for Optimizer<'_> { + type BreakTy = (); + + fn visit_expression_mut(&mut self, node: &'ast mut Expression) -> ControlFlow { + self.single_run(node); + ControlFlow::Continue(()) + } +} diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 69d82715298..091866acba0 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -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, + optimize: bool, + ) -> 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, optimize)) .collect() } else { self.suites .iter() - .map(|suite| suite.run(harness, verbose, parallel)) + .map(|suite| suite.run(harness, verbose, parallel, optimize)) .collect() }; let tests: Vec<_> = if parallel { self.tests .par_iter() - .flat_map(|test| test.run(harness, verbose)) + .flat_map(|test| test.run(harness, verbose, optimize)) .collect() } else { self.tests .iter() - .flat_map(|test| test.run(harness, verbose)) + .flat_map(|test| test.run(harness, verbose, optimize)) .collect() }; @@ -113,21 +119,21 @@ 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, optimize: bool) -> 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, optimize)); } 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, optimize)); } 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, optimize: bool) -> TestResult { let Ok(source) = Source::from_filepath(&self.path) else { if verbose > 1 { println!( @@ -185,6 +191,7 @@ impl Test { return (false, e); } context.strict(strict); + context.set_optimize(optimize); // TODO: timeout let value = match if self.is_module() { @@ -217,6 +224,8 @@ impl Test { let context = &mut Context::default(); context.strict(strict); + context.set_optimize(optimize); + if self.is_module() { match context.parse_module(source) { Ok(module_item_list) => match context.compile_module(&module_item_list) { @@ -245,6 +254,8 @@ impl Test { } => { let context = &mut Context::default(); context.strict(strict); + context.set_optimize(optimize); + 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 a6ae47a5407..19f573e6246 100644 --- a/boa_tester/src/main.rs +++ b/boa_tester/src/main.rs @@ -160,6 +160,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 +201,7 @@ fn main() -> Result<()> { test262_path, suite, output, + optimize, disable_parallelism, ignored: ignore, } => run_test_suite( @@ -206,6 +211,7 @@ fn main() -> Result<()> { suite.as_path(), output.as_deref(), ignore.as_path(), + optimize, ), Cli::Compare { base, @@ -223,6 +229,7 @@ fn run_test_suite( suite: &Path, output: Option<&Path>, ignored: &Path, + optimize: bool, ) -> Result<()> { if let Some(path) = output { if path.exists() { @@ -256,7 +263,7 @@ fn run_test_suite( if verbose != 0 { println!("Test loaded, starting..."); } - test.run(&harness, verbose); + test.run(&harness, verbose, optimize); println!(); } else { @@ -268,7 +275,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, optimize); println!(); println!("Results:"); diff --git a/test262 b/test262 index d216cc19726..9704d7f22f6 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit d216cc197269fc41eb6eca14710529c3d6650535 +Subproject commit 9704d7f22f6342d6c4753ab9a8d62d6725de8c4e