From bd9ce9935a7b3d26a26f0aef3c726fea85c1a201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Sat, 13 Jan 2018 17:42:57 +0900 Subject: [PATCH 1/2] add atoms for transformers --- atoms/build.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/atoms/build.rs b/atoms/build.rs index 787df518c30..5b43c1a820c 100644 --- a/atoms/build.rs +++ b/atoms/build.rs @@ -66,6 +66,18 @@ fn main() { "private", "protected", "public", + // Used by transforms. + "Array", + "Object", + "Infinity", + "NaN", + "function", + "string", + "number", + "boolean", + "object", + "undefined", + "length", ], ); } From 58bc3b4ade8208475c70783167ae72a825cb3f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Sat, 13 Jan 2018 18:39:10 +0900 Subject: [PATCH 2/2] [WIP] Working for simplfier --- ecmascript/Cargo.toml | 1 + ecmascript/src/lib.rs | 1 + ecmascript/transforms/Cargo.toml | 13 + ecmascript/transforms/src/lib.rs | 11 + ecmascript/transforms/src/simplify/expr.rs | 176 +++++++++++ ecmascript/transforms/src/simplify/mod.rs | 133 ++++++++ ecmascript/transforms/src/util/mod.rs | 336 +++++++++++++++++++++ ecmascript/transforms/tests/transforms.rs | 62 ++++ 8 files changed, 733 insertions(+) create mode 100644 ecmascript/transforms/Cargo.toml create mode 100644 ecmascript/transforms/src/lib.rs create mode 100644 ecmascript/transforms/src/simplify/expr.rs create mode 100644 ecmascript/transforms/src/simplify/mod.rs create mode 100644 ecmascript/transforms/src/util/mod.rs create mode 100644 ecmascript/transforms/tests/transforms.rs diff --git a/ecmascript/Cargo.toml b/ecmascript/Cargo.toml index a3022e29d41..125a9792a80 100644 --- a/ecmascript/Cargo.toml +++ b/ecmascript/Cargo.toml @@ -6,6 +6,7 @@ authors = ["강동윤 "] [dependencies] swc_ecma_ast = { path = "./ast" } swc_ecma_parser = { path = "./parser" } +swc_ecma_transforms = { path = "./transforms" } [dev-dependencies] diff --git a/ecmascript/src/lib.rs b/ecmascript/src/lib.rs index b66f90d6c1e..0c8ba9d58cb 100644 --- a/ecmascript/src/lib.rs +++ b/ecmascript/src/lib.rs @@ -1,2 +1,3 @@ pub extern crate swc_ecma_ast as ast; pub extern crate swc_ecma_parser as parser; +pub extern crate swc_ecma_transforms as transforms; diff --git a/ecmascript/transforms/Cargo.toml b/ecmascript/transforms/Cargo.toml new file mode 100644 index 00000000000..8574932f30f --- /dev/null +++ b/ecmascript/transforms/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "swc_ecma_transforms" +version = "0.1.0" +authors = ["강동윤 "] + +[dependencies] +swc_atoms = { path = "../../atoms" } +swc_common = { path = "../../common" } +swc_ecma_ast = { path = "../ast" } + +[dev-dependencies] +swc_ecma_parser = { path = "../parser" } +testing = { path = "../../testing" } \ No newline at end of file diff --git a/ecmascript/transforms/src/lib.rs b/ecmascript/transforms/src/lib.rs new file mode 100644 index 00000000000..52cc8ce3700 --- /dev/null +++ b/ecmascript/transforms/src/lib.rs @@ -0,0 +1,11 @@ +#![feature(box_patterns)] +#![feature(box_syntax)] +#![feature(specialization)] + +#[macro_use] +pub extern crate swc_atoms; +pub extern crate swc_common; +pub extern crate swc_ecma_ast as ast; + +pub mod simplify; +pub mod util; diff --git a/ecmascript/transforms/src/simplify/expr.rs b/ecmascript/transforms/src/simplify/expr.rs new file mode 100644 index 00000000000..a5beac6b93c --- /dev/null +++ b/ecmascript/transforms/src/simplify/expr.rs @@ -0,0 +1,176 @@ +use super::Simplify; +use ast::*; +use ast::{Ident, Lit}; +use ast::ExprKind::*; +use swc_common::fold::{FoldWith, Folder}; +use util::*; + +impl Folder for Simplify { + fn fold(&mut self, expr: ExprKind) -> ExprKind { + let expr = expr.fold_children(self); + + match expr { + // Do nothing for literals. + Lit(_) => expr, + + Unary { prefix, op, arg } => fold_unary(prefix, op, arg), + Binary { left, op, right } => fold_bin(left, op, right), + + Cond { test, cons, alt } => match test.as_bool() { + (p, Known(val)) => { + let expr_value = if val { cons } else { alt }; + if p.is_pure() { + expr_value.node + } else { + Seq { + exprs: vec![test, expr_value], + } + } + } + _ => Cond { test, cons, alt }, + }, + + // Simplify sequence expression. + Seq { exprs } => if exprs.len() == 1 { + exprs.into_iter().next().unwrap().node + } else { + assert!(!exprs.is_empty(), "sequence expression should not be empty"); + //TODO: remove unused + return Seq { exprs }; + }, + + // be conservative. + _ => expr, + } + } +} + +fn fold_bin(left: Box, op: BinaryOp, right: Box) -> ExprKind { + let (left, right) = match op { + BinaryOp::Add => return fold_add(left, right), + BinaryOp::LogicalAnd | BinaryOp::LogicalOr => match left.as_bool() { + (Pure, Known(val)) => { + if op == BinaryOp::LogicalAnd { + if val { + // 1 && $right + return right.node; + } else { + // 0 && $right + return Lit(Lit::Bool(false)); + } + } else { + if val { + // 1 || $right + return left.node; + } else { + // 0 || $right + return right.node; + } + } + } + _ => (left, right), + }, + BinaryOp::InstanceOf => (left, right), + + BinaryOp::Sub | BinaryOp::Div | BinaryOp::Mod => { + // Arithmetic operations + + (left, right) + } + _ => (left, right), + }; + + Binary { left, op, right } +} + +///See https://tc39.github.io/ecma262/#sec-addition-operator-plus +fn fold_add(left: Box, right: Box) -> ExprKind { + // It's string concatenation if either left or right is string. + + Binary { + left, + op: BinaryOp::Add, + right, + } +} + +fn fold_unary(prefix: bool, op: UnaryOp, arg: Box) -> ExprKind { + let span = arg.span; + + match op { + UnaryOp::TypeOf => { + let val = match arg.node { + Function(..) => "function", + Lit(Lit::Str(..)) => "string", + Lit(Lit::Num(..)) => "number", + Lit(Lit::Bool(..)) => "boolean", + Lit(Lit::Null) | Object { .. } | Array { .. } => "object", + Unary { + prefix: true, + op: UnaryOp::Void, + .. + } + | Ident(Ident { + sym: js_word!("undefined"), + .. + }) => { + // We can assume `undefined` is `undefined`, + // because overriding `undefined` is always hard error in swc. + "undefined" + } + + _ => { + return Unary { + prefix: true, + op: UnaryOp::TypeOf, + arg, + } + } + }; + + return Lit(Lit::Str(val.into())); + } + UnaryOp::Bang => match arg.as_bool() { + (p, Known(val)) => { + let new = Lit(Lit::Bool(!val)); + return if p.is_pure() { + new + } else { + Seq { + exprs: vec![arg, box Expr { span, node: new }], + } + }; + } + _ => { + return Unary { + op, + arg, + prefix: true, + } + } + }, + UnaryOp::Plus => {} + UnaryOp::Minus => match arg.node { + Ident(Ident { + sym: js_word!("Infinity"), + .. + }) => return Unary { prefix, op, arg }, + // "-NaN" is "NaN" + Ident(Ident { + sym: js_word!("NaN"), + .. + }) => return arg.node, + Lit(Lit::Num(Number(f))) => return Lit(Lit::Num(Number(-f))), + _ => { + + // TODO: Report that user is something bad (negating non-number value) + } + }, + _ => {} + } + + Unary { prefix, op, arg } +} + +/// Try to fold arithmetic binary operators +fn perform_arithmetic_op(op: BinaryOp, left: Box, right: Box) -> ExprKind {} diff --git a/ecmascript/transforms/src/simplify/mod.rs b/ecmascript/transforms/src/simplify/mod.rs new file mode 100644 index 00000000000..574eeddc339 --- /dev/null +++ b/ecmascript/transforms/src/simplify/mod.rs @@ -0,0 +1,133 @@ +//! Ported from closure compiler. +use ast::*; +use swc_common::fold::{FoldWith, Folder}; +use util::*; + +mod expr; + +#[derive(Debug, Clone, Copy)] +pub struct Simplify; + +impl Folder> for Simplify +where + Self: Folder, +{ + fn fold(&mut self, stmts: Vec) -> Vec { + let mut buf = Vec::with_capacity(stmts.len()); + + for stmt_like in stmts { + let stmt_like = self.fold(stmt_like); + let stmt_like = match stmt_like.try_into_stmt() { + Ok(stmt) => { + let span = stmt.span; + let stmt = match stmt.node { + // Remove empty statements. + StmtKind::Empty => continue, + + StmtKind::Throw { .. } + | StmtKind::Return { .. } + | StmtKind::Continue { .. } + | StmtKind::Break { .. } => { + let stmt_like = T::from_stmt(stmt); + buf.push(stmt_like); + return buf; + } + // Optimize if statement. + StmtKind::If { + test, + consequent, + alt, + } => { + // check if + let node = match test.as_bool() { + (Pure, Known(val)) => { + if val { + consequent.node + } else { + alt.map(|alt| alt.node).unwrap_or_else(|| StmtKind::Empty) + } + } + // TODO: Impure + _ => StmtKind::If { + test, + consequent, + alt, + }, + }; + Stmt { span, node } + } + _ => stmt, + }; + + T::from_stmt(stmt) + } + Err(stmt_like) => stmt_like, + }; + + buf.push(stmt_like); + } + + buf + } +} + +impl Folder for Simplify { + fn fold(&mut self, stmt: StmtKind) -> StmtKind { + let stmt = stmt.fold_children(self); + + match stmt { + // `1;` -> `;` + StmtKind::Expr(box Expr { span, node }) => match node { + ExprKind::Lit(Lit::Num(..)) + | ExprKind::Lit(Lit::Bool(..)) + | ExprKind::Lit(Lit::Regex(..)) => StmtKind::Empty, + _ => StmtKind::Expr(box Expr { node, span }), + }, + + StmtKind::Block(BlockStmt { span, stmts }) => { + if stmts.len() == 0 { + return StmtKind::Empty; + } else if stmts.len() == 1 { + // TODO: Check lexical variable + return stmts.into_iter().next().unwrap().node; + } else { + StmtKind::Block(BlockStmt { span, stmts }) + } + } + + _ => stmt, + } + } +} + +pub trait StmtLike: Sized { + fn try_into_stmt(self) -> Result; + fn from_stmt(stmt: Stmt) -> Self; +} + +impl StmtLike for Stmt { + fn try_into_stmt(self) -> Result { + Ok(self) + } + fn from_stmt(stmt: Stmt) -> Self { + stmt + } +} + +impl StmtLike for ModuleItem { + fn try_into_stmt(self) -> Result { + match self { + ModuleItem::Stmt(stmt) => Ok(stmt), + _ => Err(self), + } + } + fn from_stmt(stmt: Stmt) -> Self { + ModuleItem::Stmt(stmt) + } +} + +// impl Folder for Simplify { +// fn fold(&mut self, stmt: Stmt) -> Stmt { +// stmt.fold_children(&mut FoldConst) +// } +// } diff --git a/ecmascript/transforms/src/util/mod.rs b/ecmascript/transforms/src/util/mod.rs new file mode 100644 index 00000000000..5aa65330831 --- /dev/null +++ b/ecmascript/transforms/src/util/mod.rs @@ -0,0 +1,336 @@ +pub use self::Purity::{MayBeImpure, Pure}; +pub use self::Value::{Known, Unknown}; +use ast::*; +use std::borrow::Cow; +use std::f64::{INFINITY, NAN}; +use std::num::FpCategory; +use std::ops::{Add, Not}; + +pub trait ExprExt: Sized + AsRef { + /// + /// This method emulates the `Boolean()` JavaScript cast function. + ///Note: unlike getPureBooleanValue this function does not return `None` + ///for expressions with side-effects. + fn as_bool(&self) -> (Purity, Bool) { + let expr = self.as_ref(); + let val = match expr.node { + ExprKind::Paren(ref e) => return e.as_bool(), + ExprKind::Seq { ref exprs } => return exprs.last().unwrap().as_bool(), + ExprKind::Assign { ref right, .. } => return right.as_bool(), + + ExprKind::Unary { + prefix: true, + op: UnaryOp::Bang, + ref arg, + } => { + let (p, v) = arg.as_bool(); + return (p, !v); + } + + ExprKind::Binary { + ref left, + op: op @ BinaryOp::LogicalAnd, + ref right, + } + | ExprKind::Binary { + ref left, + op: op @ BinaryOp::LogicalOr, + ref right, + } => { + // TODO: Ignore purity if value cannot be reached. + + let (lp, lv) = left.as_bool(); + let (rp, rv) = right.as_bool(); + + if lp + rp == Pure { + return (Pure, lv.and(rv)); + } + if op == BinaryOp::LogicalAnd { + lv.and(rv) + } else { + lv.or(rv) + } + } + + ExprKind::Function(..) + | ExprKind::Class(..) + | ExprKind::New { .. } + | ExprKind::Array { .. } + | ExprKind::Object { .. } => Known(true), + + ExprKind::Unary { + prefix: true, + op: UnaryOp::Void, + arg: _, + } => Known(false), + + ExprKind::Lit(ref lit) => { + return ( + Pure, + Known(match *lit { + Lit::Num(Number(n)) => match n.classify() { + FpCategory::Nan | FpCategory::Zero => false, + _ => true, + }, + Lit::Bool(b) => b, + Lit::Str(ref s) => !s.is_empty(), + Lit::Null => false, + Lit::Regex(..) => true, + }), + ) + } + + //TODO? + _ => Unknown, + }; + + (MayBeImpure, val) + } + + /// Emulates javascript Number() cast function. + fn as_number(&self) -> Value { + let expr = self.as_ref(); + let v = match expr.node { + ExprKind::Lit(ref l) => match *l { + Lit::Bool(true) => 1.0, + Lit::Bool(false) | Lit::Null => 0.0, + Lit::Num(Number(n)) => n, + Lit::Str(ref s) => return num_from_str(s), + _ => return Unknown, + }, + ExprKind::Ident(Ident { ref sym, .. }) => match &**sym { + "undefined" | "NaN" => NAN, + "Infinity" => INFINITY, + _ => return Unknown, + }, + ExprKind::Unary { + prefix: true, + op: UnaryOp::Minus, + arg: + box Expr { + span: _, + node: + ExprKind::Ident(Ident { + sym: js_word!("Infinity"), + .. + }), + }, + } => -INFINITY, + ExprKind::Unary { + prefix: true, + op: UnaryOp::Bang, + ref arg, + } => match arg.as_bool() { + (Pure, Known(v)) => { + if v { + 0.0 + } else { + 1.0 + } + } + _ => return Unknown, + }, + ExprKind::Unary { + prefix: true, + op: UnaryOp::Void, + ref arg, + } => { + if arg.may_have_side_effects() { + return Unknown; + } else { + NAN + } + } + + ExprKind::Tpl(..) | ExprKind::Object { .. } | ExprKind::Array { .. } => { + match self.as_string() { + Some(ref s) => return num_from_str(s), + None => return Unknown, + } + } + + _ => return Unknown, + }; + + Known(v) + } + + fn as_string(&self) -> Option> { + let expr = self.as_ref(); + match expr.node { + ExprKind::Lit(ref l) => match *l { + Lit::Str(ref s) => Some(Cow::Borrowed(s)), + Lit::Num(n) => Some(format!("{}", n).into()), + Lit::Bool(true) => Some(Cow::Borrowed("true")), + Lit::Bool(false) => Some(Cow::Borrowed("false")), + Lit::Null => Some(Cow::Borrowed("null")), + _ => None, + }, + ExprKind::Tpl(_) => { + // TODO: + // Only convert a template literal if all its expressions can be converted. + unimplemented!("TplLit.as_string()") + } + ExprKind::Ident(Ident { ref sym, .. }) => match &**sym { + "undefined" | "Infinity" | "NaN" => Some(Cow::Borrowed(&**sym)), + _ => None, + }, + ExprKind::Unary { + prefix: true, + op: UnaryOp::Void, + .. + } => Some(Cow::Borrowed("undefined")), + ExprKind::Unary { + prefix: true, + op: UnaryOp::Bang, + ref arg, + } => match arg.as_bool() { + (Pure, Known(v)) => Some(Cow::Borrowed(if v { "false" } else { "true" })), + _ => None, + }, + ExprKind::Array { ref elems } => { + let mut first = true; + let mut buf = String::new(); + // null, undefined is "" in array literl. + for elem in elems { + let e = match *elem { + Some(ref elem) => match *elem { + ExprOrSpread::Expr(ref e) | ExprOrSpread::Spread(ref e) => match e.node + { + ExprKind::Lit(Lit::Null) + | ExprKind::Ident(Ident { + sym: js_word!("undefined"), + .. + }) => Cow::Borrowed(""), + _ => match e.as_string() { + Some(s) => s, + None => return None, + }, + }, + }, + None => Cow::Borrowed(""), + }; + buf.push_str(&e); + + if first { + first = false; + } else { + buf.push(','); + } + } + Some(buf.into()) + } + ExprKind::Object { .. } => Some(Cow::Borrowed("[object Object]")), + _ => None, + } + } +} + +fn num_from_str(s: &str) -> Value { + if s.contains('\u{000b}') { + return Unknown; + } + + // TODO: Check if this is correct + let s = s.trim(); + + if s.is_empty() { + return Known(0.0); + } + + if s.starts_with("0x") || s.starts_with("0X") { + return match s[2..4].parse() { + Ok(n) => Known(n), + Err(_) => Known(NAN), + }; + } + + if (s.starts_with('-') || s.starts_with('+')) + && (s[1..].starts_with("0x") || s[1..].starts_with("0X")) + { + // hex numbers with explicit signs vary between browsers. + return Unknown; + } + + // Firefox and IE treat the "Infinity" differently. Firefox is case + // insensitive, but IE treats "infinity" as NaN. So leave it alone. + match s { + "infinity" | "+infinity" | "-infinity" => return Unknown, + _ => {} + } + + Known(s.parse().ok().unwrap_or(NAN)) +} + +impl> ExprExt for T {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Purity { + MayBeImpure, + Pure, +} +impl Purity { + pub fn is_pure(self) -> bool { + self == Pure + } +} + +impl Add for Purity { + type Output = Self; + fn add(self, rhs: Self) -> Self { + match (self, rhs) { + (Pure, Pure) => Pure, + _ => MayBeImpure, + } + } +} + +/// Value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Value { + Known(T), + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Type { + Str, + Obj, + Undetermined, +} + +pub type Bool = Value; + +impl Value { + pub fn and(self, other: Self) -> Self { + match self { + Known(true) => other, + Known(false) => Known(false), + Unknown => match other { + Known(false) => Known(false), + _ => Unknown, + }, + } + } + + pub fn or(self, other: Self) -> Self { + match self { + Known(true) => Known(true), + Known(false) => other, + Unknown => match other { + Known(true) => Known(true), + _ => Unknown, + }, + } + } +} + +impl Not for Value { + type Output = Self; + fn not(self) -> Self { + match self { + Value::Known(b) => Value::Known(!b), + Value::Unknown => Value::Unknown, + } + } +} diff --git a/ecmascript/transforms/tests/transforms.rs b/ecmascript/transforms/tests/transforms.rs new file mode 100644 index 00000000000..cc77966b890 --- /dev/null +++ b/ecmascript/transforms/tests/transforms.rs @@ -0,0 +1,62 @@ +extern crate swc_common; +extern crate swc_ecma_ast as ast; +extern crate swc_ecma_parser; +extern crate swc_ecma_transforms as transforms; +#[macro_use] +extern crate testing; + +use swc_common::fold::Folder; +use swc_ecma_parser::lexer::Lexer; +use swc_ecma_parser::parser::Parser; +use testing::logger; +use transforms::simplify::Simplify; + +macro_rules! parse { + ($s:expr) => {{ + let l = logger(); + Parser::new_for_module(l.clone(), Lexer::new_from_str(l, $s)) + .parse_module() + .unwrap_or_else(|err| panic!("failed to parse module: {:?}", err)) + }}; +} + +fn fold(t: T, mut f: F) -> T +where + F: Folder, +{ + f.fold(t) +} + +#[test] +fn add_simple() { + assert_eq_ignore_span!(fold(parse!("use(3 + 6)"), Simplify), parse!("use(9)")) +} + +#[test] +fn cond_simple() { + assert_eq_ignore_span!( + fold(parse!("use(true ? 3 : 6)"), Simplify), + parse!("use(3)") + ); + assert_eq_ignore_span!( + fold(parse!("use(false ? 3 : 6)"), Simplify), + parse!("use(6)") + ); +} + +#[test] +fn cond_side_effect() { + assert_eq_ignore_span!( + fold(parse!("new UnknownClass() ? 3 : 6"), Simplify), + parse!("new UnknownClass(), 3") + ); + assert_eq_ignore_span!( + fold(parse!("(void fn()) ? 3 : 6"), Simplify), + parse!("(void fn()), 6") + ); +} + +#[test] +fn oror_non_bool() { + assert_eq_ignore_span!(fold(parse!("use(5 || 50)"), Simplify), parse!("use(5)")) +}