mirror of https://github.com/boa-dev/boa.git
Browse Source
This PR implements an optimizer, It currently implements the [constant folding optimization][cfo]. this optimization is responsible for "folding"/evaluating constant expressions. For example: ```js let x = ((1 + 2 + -4) * 8) << 4 ``` Generates the following instruction(s) (`cargo run -- -t`): ``` 000000 0000 PushOne 000001 0001 PushInt8 2 000003 0002 Add 000004 0003 PushInt8 4 000006 0004 Neg 000007 0005 Add 000008 0006 PushInt8 8 000010 0007 Mul 000011 0008 PushInt8 4 000013 0009 ShiftLeft 000014 0010 DefInitLet 0000: 'x' ``` With constant folding it generates the following instruction(s) (`cargo run -- -t -O`): ``` 000000 0000 PushInt8 -128 000002 0001 DefInitLet 0000: 'x' ``` It changes the following: - Implement ~~WIP~~ constant folding optimization, ~~only works with integers for now~~ - Add `--optimize, -O` flag to boa_cli - Add `--optimizer-statistics` flag to boa_cli for optimizer statistics - Add `--optimize, -O` flag to boa_tester After I finish with this, will try to implement other optimizations :) [cfo]: https://en.wikipedia.org/wiki/Constant_foldingpull/2765/head
Haled Odat
2 years ago
16 changed files with 592 additions and 35 deletions
@ -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<T> { |
||||
/// 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<Expression> { |
||||
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::BreakTy> { |
||||
self.run_all(node); |
||||
ControlFlow::Continue(()) |
||||
} |
||||
} |
@ -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<Expression> { |
||||
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<Expression> { |
||||
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<Expression> { |
||||
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))) |
||||
} |
||||
} |
@ -0,0 +1,3 @@
|
||||
mod constant_folding; |
||||
|
||||
pub(crate) use constant_folding::ConstantFolding; |
@ -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<F> |
||||
where |
||||
F: FnMut(&mut Expression) -> PassAction<Expression>, |
||||
{ |
||||
/// The function to be applied to the node.
|
||||
f: F, |
||||
|
||||
/// Did a change happen while traversing.
|
||||
changed: bool, |
||||
} |
||||
|
||||
impl<F> Walker<F> |
||||
where |
||||
F: FnMut(&mut Expression) -> PassAction<Expression>, |
||||
{ |
||||
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<F> |
||||
where |
||||
F: FnMut(&mut Expression) -> PassAction<Expression>, |
||||
{ |
||||
type BreakTy = Infallible; |
||||
|
||||
/// Visits the tree in postorder.
|
||||
fn visit_expression_mut(&mut self, expr: &'ast mut Expression) -> ControlFlow<Self::BreakTy> { |
||||
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(()) |
||||
} |
||||
} |
Loading…
Reference in new issue