[WIP] Working for simplfier

This commit is contained in:
강동윤 2018-01-13 18:39:10 +09:00
parent 8d62017d88
commit 015ccb07f4
8 changed files with 733 additions and 0 deletions

View File

@ -6,6 +6,7 @@ authors = ["강동윤 <kdy1@outlook.kr>"]
[dependencies]
swc_ecma_ast = { path = "./ast" }
swc_ecma_parser = { path = "./parser" }
swc_ecma_transforms = { path = "./transforms" }
[dev-dependencies]

View File

@ -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;

View File

@ -0,0 +1,13 @@
[package]
name = "swc_ecma_transforms"
version = "0.1.0"
authors = ["강동윤 <kdy1@outlook.kr>"]
[dependencies]
swc_atoms = { path = "../../atoms" }
swc_common = { path = "../../common" }
swc_ecma_ast = { path = "../ast" }
[dev-dependencies]
swc_ecma_parser = { path = "../parser" }
testing = { path = "../../testing" }

View File

@ -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;

View File

@ -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<ExprKind> 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<Expr>, op: BinaryOp, right: Box<Expr>) -> 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<Expr>, right: Box<Expr>) -> 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<Expr>) -> 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<Expr>, right: Box<Expr>) -> ExprKind {}

View File

@ -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<T: StmtLike> Folder<Vec<T>> for Simplify
where
Self: Folder<T>,
{
fn fold(&mut self, stmts: Vec<T>) -> Vec<T> {
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<StmtKind> 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<Stmt, Self>;
fn from_stmt(stmt: Stmt) -> Self;
}
impl StmtLike for Stmt {
fn try_into_stmt(self) -> Result<Stmt, Self> {
Ok(self)
}
fn from_stmt(stmt: Stmt) -> Self {
stmt
}
}
impl StmtLike for ModuleItem {
fn try_into_stmt(self) -> Result<Stmt, Self> {
match self {
ModuleItem::Stmt(stmt) => Ok(stmt),
_ => Err(self),
}
}
fn from_stmt(stmt: Stmt) -> Self {
ModuleItem::Stmt(stmt)
}
}
// impl Folder<Stmt> for Simplify {
// fn fold(&mut self, stmt: Stmt) -> Stmt {
// stmt.fold_children(&mut FoldConst)
// }
// }

View File

@ -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<Expr> {
///
/// 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<f64> {
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<Cow<str>> {
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<f64> {
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<T: AsRef<Expr>> 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<T> {
Known(T),
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Type {
Str,
Obj,
Undetermined,
}
pub type Bool = Value<bool>;
impl Value<bool> {
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<bool> {
type Output = Self;
fn not(self) -> Self {
match self {
Value::Known(b) => Value::Known(!b),
Value::Unknown => Value::Unknown,
}
}
}

View File

@ -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, F>(t: T, mut f: F) -> T
where
F: Folder<T>,
{
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)"))
}