From 18059cc94fdc037e296a1cb1b08143d5e3aae570 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 3 Mar 2017 18:04:32 +0800 Subject: [PATCH] compress numerical expressions (#1513) safe operations - `a === b` => `a == b` - `a + -b` => `a - b` - `-a + b` => `b - a` - `a+ +b` => `+b+a` associative operations (bit-wise operations are safe, otherwise `unsafe_math`) - `a + (b + c)` => `(a + b) + c` - `(n + 2) + 3` => `5 + n` - `(2 * n) * 3` => `6 * n` - `(a | 1) | (2 | d)` => `(3 | a) | b` fixes #412 --- README.md | 3 + lib/compress.js | 171 +++++++++++++++++++++++++++++++++++++-- test/compress/numbers.js | 136 +++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 79064d79..628bcdec 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). comparison are switching. Compression only works if both `comparisons` and `unsafe_comps` are both set to true. +- `unsafe_math` (default: false) -- optimize numerical expressions like + `2 * x * 3` into `6 * x`, which may give imprecise floating point results. + - `unsafe_proto` (default: false) -- optimize expressions like `Array.prototype.slice.call(a)` into `[].slice.call(a)` diff --git a/lib/compress.js b/lib/compress.js index 38ebbf40..ec1e7174 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -54,6 +54,7 @@ function Compressor(options, false_by_default) { drop_debugger : !false_by_default, unsafe : false, unsafe_comps : false, + unsafe_math : false, unsafe_proto : false, conditionals : !false_by_default, comparisons : !false_by_default, @@ -1043,6 +1044,34 @@ merge(Compressor.prototype, { node.DEFMETHOD("is_boolean", func); }); + // methods to determine if an expression has a numeric result type + (function (def){ + def(AST_Node, return_false); + def(AST_Number, return_true); + var unary = makePredicate("+ - ~ ++ --"); + def(AST_Unary, function(){ + return unary(this.operator); + }); + var binary = makePredicate("- * / % & | ^ << >> >>>"); + def(AST_Binary, function(compressor){ + return binary(this.operator) || this.operator == "+" + && this.left.is_number(compressor) + && this.right.is_number(compressor); + }); + var assign = makePredicate("-= *= /= %= &= |= ^= <<= >>= >>>="); + def(AST_Assign, function(compressor){ + return assign(this.operator) || this.right.is_number(compressor); + }); + def(AST_Seq, function(compressor){ + return this.cdr.is_number(compressor); + }); + def(AST_Conditional, function(compressor){ + return this.consequent.is_number(compressor) && this.alternative.is_number(compressor); + }); + })(function(node, func){ + node.DEFMETHOD("is_number", func); + }); + // methods to determine if an expression has a string result type (function (def){ def(AST_Node, return_false); @@ -2867,8 +2896,14 @@ merge(Compressor.prototype, { right: rhs[0] }).optimize(compressor); } - function reverse(op, force) { - if (force || !(self.left.has_side_effects(compressor) || self.right.has_side_effects(compressor))) { + function reversible() { + return self.left instanceof AST_Constant + || self.right instanceof AST_Constant + || !self.left.has_side_effects(compressor) + && !self.right.has_side_effects(compressor); + } + function reverse(op) { + if (reversible()) { if (op) self.operator = op; var tmp = self.left; self.left = self.right; @@ -2884,7 +2919,7 @@ merge(Compressor.prototype, { if (!(self.left instanceof AST_Binary && PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) { - reverse(null, true); + reverse(); } } if (/^[!=]==?$/.test(self.operator)) { @@ -2919,6 +2954,7 @@ merge(Compressor.prototype, { case "===": case "!==": if ((self.left.is_string(compressor) && self.right.is_string(compressor)) || + (self.left.is_number(compressor) && self.right.is_number(compressor)) || (self.left.is_boolean() && self.right.is_boolean())) { self.operator = self.operator.substr(0, 2); } @@ -3056,7 +3092,10 @@ merge(Compressor.prototype, { } break; } - if (self.operator == "+") { + var associative = true; + switch (self.operator) { + case "+": + // "foo" + ("bar" + x) => "foobar" + x if (self.left instanceof AST_Constant && self.right instanceof AST_Binary && self.right.operator == "+" @@ -3064,7 +3103,7 @@ merge(Compressor.prototype, { && self.right.is_string(compressor)) { self = make_node(AST_Binary, self, { operator: "+", - left: make_node(AST_String, null, { + left: make_node(AST_String, self.left, { value: "" + self.left.getValue() + self.right.left.getValue(), start: self.left.start, end: self.right.left.end @@ -3072,6 +3111,7 @@ merge(Compressor.prototype, { right: self.right.right }); } + // (x + "foo") + "bar" => x + "foobar" if (self.right instanceof AST_Constant && self.left instanceof AST_Binary && self.left.operator == "+" @@ -3080,13 +3120,14 @@ merge(Compressor.prototype, { self = make_node(AST_Binary, self, { operator: "+", left: self.left.left, - right: make_node(AST_String, null, { + right: make_node(AST_String, self.right, { value: "" + self.left.right.getValue() + self.right.getValue(), start: self.left.right.start, end: self.right.end }) }); } + // (x + "foo") + ("bar" + y) => (x + "foobar") + y if (self.left instanceof AST_Binary && self.left.operator == "+" && self.left.is_string(compressor) @@ -3100,7 +3141,7 @@ merge(Compressor.prototype, { left: make_node(AST_Binary, self.left, { operator: "+", left: self.left.left, - right: make_node(AST_String, null, { + right: make_node(AST_String, self.left.right, { value: "" + self.left.right.getValue() + self.right.left.getValue(), start: self.left.right.start, end: self.right.left.end @@ -3109,6 +3150,122 @@ merge(Compressor.prototype, { right: self.right.right }); } + // a + -b => a - b + if (self.right instanceof AST_UnaryPrefix + && self.right.operator == "-" + && self.left.is_number(compressor)) { + self = make_node(AST_Binary, self, { + operator: "-", + left: self.left, + right: self.right.expression + }); + } + // -a + b => b - a + if (self.left instanceof AST_UnaryPrefix + && self.left.operator == "-" + && reversible() + && self.right.is_number(compressor)) { + self = make_node(AST_Binary, self, { + operator: "-", + left: self.right, + right: self.left.expression + }); + } + case "*": + associative = compressor.option("unsafe_math"); + case "&": + case "|": + case "^": + // a + +b => +b + a + if (self.left.is_number(compressor) + && self.right.is_number(compressor) + && reversible() + && !(self.left instanceof AST_Binary + && self.left.operator != self.operator + && PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) { + var reversed = make_node(AST_Binary, self, { + operator: self.operator, + left: self.right, + right: self.left + }); + if (self.right instanceof AST_Constant + && !(self.left instanceof AST_Constant)) { + self = best_of(reversed, self); + } else { + self = best_of(self, reversed); + } + } + if (associative && self.is_number(compressor)) { + // a + (b + c) => (a + b) + c + if (self.right instanceof AST_Binary + && self.right.operator == self.operator) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left, + right: self.right.left, + start: self.left.start, + end: self.right.left.end + }), + right: self.right.right + }); + } + // (n + 2) + 3 => 5 + n + // (2 * n) * 3 => 6 + n + if (self.right instanceof AST_Constant + && self.left instanceof AST_Binary + && self.left.operator == self.operator) { + if (self.left.left instanceof AST_Constant) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left.left, + right: self.right, + start: self.left.left.start, + end: self.right.end + }), + right: self.left.right + }); + } else if (self.left.right instanceof AST_Constant) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left.right, + right: self.right, + start: self.left.right.start, + end: self.right.end + }), + right: self.left.left + }); + } + } + // (a | 1) | (2 | d) => (3 | a) | b + if (self.left instanceof AST_Binary + && self.left.operator == self.operator + && self.left.right instanceof AST_Constant + && self.right instanceof AST_Binary + && self.right.operator == self.operator + && self.right.left instanceof AST_Constant) { + self = make_node(AST_Binary, self, { + operator: self.operator, + left: make_node(AST_Binary, self.left, { + operator: self.operator, + left: make_node(AST_Binary, self.left.left, { + operator: self.operator, + left: self.left.right, + right: self.right.left, + start: self.left.right.start, + end: self.right.left.end + }), + right: self.left.left + }), + right: self.right.right + }); + } + } } } // x && (y && z) ==> x && y && z diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 8e32ad02..0b40bb9c 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -17,3 +17,139 @@ hex_numbers_in_parentheses_for_prototype_functions: { } expect_exact: "-2;(-2).toFixed(0);2;2..toFixed(0);.2;.2.toFixed(0);2e-8;2e-8.toFixed(0);0xde0b6b3a7640080;(0xde0b6b3a7640080).toFixed(0);" } + +comparisons: { + options = { + comparisons: true, + } + input: { + console.log( + ~x === 42, + x % n === 42 + ); + } + expect: { + console.log( + 42 == ~x, + x % n == 42 + ); + } +} + +evaluate_1: { + options = { + evaluate: true, + unsafe_math: false, + } + input: { + console.log( + x + 1 + 2, + x * 1 * 2, + +x + 1 + 2, + 1 + x + 2 + 3, + 1 | x | 2 | 3, + 1 + x-- + 2 + 3, + 1 + (x*y + 2) + 3, + 1 + (2 + x + 3), + 1 + (2 + ~x + 3), + -y + (2 + ~x + 3), + 1 & (2 & x & 3), + 1 + (2 + (x |= 0) + 3) + ); + } + expect: { + console.log( + x + 1 + 2, + 1 * x * 2, + +x + 1 + 2, + 1 + x + 2 + 3, + 3 | x, + 1 + x-- + 2 + 3, + x*y + 2 + 1 + 3, + 1 + (2 + x + 3), + 2 + ~x + 3 + 1, + -y + (2 + ~x + 3), + 0 & x, + 2 + (x |= 0) + 3 + 1 + ); + } +} + +evaluate_2: { + options = { + evaluate: true, + unsafe_math: true, + } + input: { + console.log( + x + 1 + 2, + x * 1 * 2, + +x + 1 + 2, + 1 + x + 2 + 3, + 1 | x | 2 | 3, + 1 + x-- + 2 + 3, + 1 + (x*y + 2) + 3, + 1 + (2 + x + 3), + 1 & (2 & x & 3), + 1 + (2 + (x |= 0) + 3) + ); + } + expect: { + console.log( + x + 1 + 2, + 2 * x, + 3 + +x, + 1 + x + 2 + 3, + 3 | x, + 6 + x--, + 6 + x*y, + 1 + (2 + x + 3), + 0 & x, + 6 + (x |= 0) + ); + } +} + +evaluate_3: { + options = { + evaluate: true, + unsafe: true, + unsafe_math: true, + } + input: { + console.log(1 + Number(x) + 2); + } + expect: { + console.log(3 + +x); + } +} + +evaluate_4: { + options = { + evaluate: true, + } + input: { + console.log( + 1+ +a, + +a+1, + 1+-a, + -a+1, + +a+ +b, + +a+-b, + -a+ +b, + -a+-b + ); + } + expect: { + console.log( + +a+1, + +a+1, + 1-a, + 1-a, + +a+ +b, + +a-b, + -a+ +b, + -a-b + ); + } +} -- 2.34.1