Browse Source

Implement constant folding optimization (#2679)

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_folding
pull/2765/head
Haled Odat 2 years ago
parent
commit
6c438b5516
  1. 7
      boa_ast/src/expression/literal/array.rs
  2. 7
      boa_ast/src/expression/literal/mod.rs
  3. 14
      boa_ast/src/expression/operator/binary/mod.rs
  4. 7
      boa_ast/src/expression/operator/unary/mod.rs
  5. 60
      boa_cli/src/main.rs
  6. 16
      boa_engine/benches/full.rs
  7. 1
      boa_engine/src/bytecompiler/expression/mod.rs
  8. 3
      boa_engine/src/bytecompiler/mod.rs
  9. 31
      boa_engine/src/context/mod.rs
  10. 2
      boa_engine/src/lib.rs
  11. 135
      boa_engine/src/optimizer/mod.rs
  12. 229
      boa_engine/src/optimizer/pass/constant_folding.rs
  13. 3
      boa_engine/src/optimizer/pass/mod.rs
  14. 59
      boa_engine/src/optimizer/walker.rs
  15. 37
      boa_tester/src/exec/mod.rs
  16. 16
      boa_tester/src/main.rs

7
boa_ast/src/expression/literal/array.rs

@ -159,6 +159,13 @@ impl AsRef<[Option<Expression>]> for ArrayLiteral {
}
}
impl AsMut<[Option<Expression>]> for ArrayLiteral {
#[inline]
fn as_mut(&mut self) -> &mut [Option<Expression>] {
&mut self.arr
}
}
impl<T> From<T> for ArrayLiteral
where
T: Into<Box<[Option<Expression>]>>,

7
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<Sym> 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(),
}
}
}

14
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 {

7
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 {

60
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(())

16
boa_engine/benches/full.rs

@ -1,6 +1,8 @@
//! Benchmarks of the whole execution engine in Boa.
use boa_engine::{context::DefaultHooks, realm::Realm, Context, Source};
use boa_engine::{
context::DefaultHooks, optimizer::OptimizerOptions, realm::Realm, Context, Source,
};
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
@ -24,6 +26,10 @@ macro_rules! full_benchmarks {
{
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
// Disable optimizations
context.set_optimizer_options(OptimizerOptions::empty());
c.bench_function(concat!($id, " (Parser)"), move |b| {
b.iter(|| context.parse_script(black_box(Source::from_bytes(CODE))))
});
@ -35,6 +41,10 @@ macro_rules! full_benchmarks {
{
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
// Disable optimizations
context.set_optimizer_options(OptimizerOptions::empty());
let statement_list = context.parse_script(Source::from_bytes(CODE)).expect("parsing failed");
c.bench_function(concat!($id, " (Compiler)"), move |b| {
b.iter(|| {
@ -49,6 +59,10 @@ macro_rules! full_benchmarks {
{
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
// Disable optimizations
context.set_optimizer_options(OptimizerOptions::empty());
let statement_list = context.parse_script(Source::from_bytes(CODE)).expect("parsing failed");
let code_block = context.compile_script(&statement_list).unwrap();
c.bench_function(concat!($id, " (Execution)"), move |b| {

1
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 {

3
boa_engine/src/bytecompiler/mod.rs

@ -499,8 +499,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);

31
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("optimizer_options", &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<R: Read>(
&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.
@ -427,6 +444,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;
@ -643,6 +669,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::OPTIMIZE_ALL,
};
builtins::set_default_global_bindings(&mut context)?;

2
boa_engine/src/lib.rs

@ -158,6 +158,8 @@ pub mod symbol;
pub mod value;
pub mod vm;
pub mod optimizer;
#[cfg(feature = "console")]
pub mod console;

135
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<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(())
}
}

229
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<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)))
}
}

3
boa_engine/src/optimizer/pass/mod.rs

@ -0,0 +1,3 @@
mod constant_folding;
pub(crate) use constant_folding::ConstantFolding;

59
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<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(())
}
}

37
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 fxhash::FxHashSet;
@ -24,6 +24,7 @@ impl TestSuite {
verbose: u8,
parallel: bool,
max_edition: SpecEdition,
optimizer_options: OptimizerOptions,
) -> SuiteResult {
if verbose != 0 {
println!("Suite {}:", self.path.display());
@ -32,12 +33,12 @@ impl TestSuite {
let suites: Vec<_> = if parallel {
self.suites
.par_iter()
.map(|suite| suite.run(harness, verbose, parallel, max_edition))
.map(|suite| suite.run(harness, verbose, parallel, max_edition, optimizer_options))
.collect()
} else {
self.suites
.iter()
.map(|suite| suite.run(harness, verbose, parallel, max_edition))
.map(|suite| suite.run(harness, verbose, parallel, max_edition, optimizer_options))
.collect()
};
@ -45,13 +46,13 @@ impl TestSuite {
self.tests
.par_iter()
.filter(|test| test.edition <= max_edition)
.flat_map(|test| test.run(harness, verbose))
.flat_map(|test| test.run(harness, verbose, optimizer_options))
.collect()
} else {
self.tests
.iter()
.filter(|test| test.edition <= max_edition)
.flat_map(|test| test.run(harness, verbose))
.flat_map(|test| test.run(harness, verbose, optimizer_options))
.collect()
};
@ -134,21 +135,32 @@ impl TestSuite {
impl Test {
/// Runs the test.
pub(crate) fn run(&self, harness: &Harness, verbose: u8) -> Vec<TestResult> {
pub(crate) fn run(
&self,
harness: &Harness,
verbose: u8,
optimizer_options: OptimizerOptions,
) -> Vec<TestResult> {
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!(
@ -208,6 +220,7 @@ impl Test {
return (false, e);
}
context.strict(strict);
context.set_optimizer_options(optimizer_options);
// TODO: timeout
let value = match if self.is_module() {
@ -247,6 +260,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) {
@ -275,6 +290,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);
}

16
boa_tester/src/main.rs

@ -76,6 +76,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, eyre, WrapErr},
@ -163,6 +164,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<PathBuf>,
@ -208,6 +213,7 @@ fn main() -> Result<()> {
test262_path,
suite,
output,
optimize,
disable_parallelism,
ignored: ignore,
edition,
@ -221,6 +227,11 @@ fn main() -> Result<()> {
ignore.as_path(),
edition.unwrap_or_default(),
versioned,
if optimize {
OptimizerOptions::OPTIMIZE_ALL
} else {
OptimizerOptions::empty()
},
),
Cli::Compare {
base,
@ -241,6 +252,7 @@ fn run_test_suite(
ignore: &Path,
edition: SpecEdition,
versioned: bool,
optimizer_options: OptimizerOptions,
) -> Result<()> {
if let Some(path) = output {
if path.exists() {
@ -275,7 +287,7 @@ fn run_test_suite(
if verbose != 0 {
println!("Test loaded, starting...");
}
test.run(&harness, verbose);
test.run(&harness, verbose, optimizer_options);
} else {
println!(
"Minimum spec edition of test is bigger than the specified edition. Skipping."
@ -292,7 +304,7 @@ fn run_test_suite(
if verbose != 0 {
println!("Test suite loaded, starting tests...");
}
let results = suite.run(&harness, verbose, parallel, edition);
let results = suite.run(&harness, verbose, parallel, edition, optimizer_options);
if versioned {
let mut table = comfy_table::Table::new();

Loading…
Cancel
Save