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
This commit is contained in:
@@ -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
|
comparison are switching. Compression only works if both `comparisons` and
|
||||||
`unsafe_comps` are both set to true.
|
`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
|
- `unsafe_proto` (default: false) -- optimize expressions like
|
||||||
`Array.prototype.slice.call(a)` into `[].slice.call(a)`
|
`Array.prototype.slice.call(a)` into `[].slice.call(a)`
|
||||||
|
|
||||||
|
|||||||
171
lib/compress.js
171
lib/compress.js
@@ -54,6 +54,7 @@ function Compressor(options, false_by_default) {
|
|||||||
drop_debugger : !false_by_default,
|
drop_debugger : !false_by_default,
|
||||||
unsafe : false,
|
unsafe : false,
|
||||||
unsafe_comps : false,
|
unsafe_comps : false,
|
||||||
|
unsafe_math : false,
|
||||||
unsafe_proto : false,
|
unsafe_proto : false,
|
||||||
conditionals : !false_by_default,
|
conditionals : !false_by_default,
|
||||||
comparisons : !false_by_default,
|
comparisons : !false_by_default,
|
||||||
@@ -1043,6 +1044,34 @@ merge(Compressor.prototype, {
|
|||||||
node.DEFMETHOD("is_boolean", func);
|
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
|
// methods to determine if an expression has a string result type
|
||||||
(function (def){
|
(function (def){
|
||||||
def(AST_Node, return_false);
|
def(AST_Node, return_false);
|
||||||
@@ -2867,8 +2896,14 @@ merge(Compressor.prototype, {
|
|||||||
right: rhs[0]
|
right: rhs[0]
|
||||||
}).optimize(compressor);
|
}).optimize(compressor);
|
||||||
}
|
}
|
||||||
function reverse(op, force) {
|
function reversible() {
|
||||||
if (force || !(self.left.has_side_effects(compressor) || self.right.has_side_effects(compressor))) {
|
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;
|
if (op) self.operator = op;
|
||||||
var tmp = self.left;
|
var tmp = self.left;
|
||||||
self.left = self.right;
|
self.left = self.right;
|
||||||
@@ -2884,7 +2919,7 @@ merge(Compressor.prototype, {
|
|||||||
|
|
||||||
if (!(self.left instanceof AST_Binary
|
if (!(self.left instanceof AST_Binary
|
||||||
&& PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) {
|
&& PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) {
|
||||||
reverse(null, true);
|
reverse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (/^[!=]==?$/.test(self.operator)) {
|
if (/^[!=]==?$/.test(self.operator)) {
|
||||||
@@ -2919,6 +2954,7 @@ merge(Compressor.prototype, {
|
|||||||
case "===":
|
case "===":
|
||||||
case "!==":
|
case "!==":
|
||||||
if ((self.left.is_string(compressor) && self.right.is_string(compressor)) ||
|
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.left.is_boolean() && self.right.is_boolean())) {
|
||||||
self.operator = self.operator.substr(0, 2);
|
self.operator = self.operator.substr(0, 2);
|
||||||
}
|
}
|
||||||
@@ -3056,7 +3092,10 @@ merge(Compressor.prototype, {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (self.operator == "+") {
|
var associative = true;
|
||||||
|
switch (self.operator) {
|
||||||
|
case "+":
|
||||||
|
// "foo" + ("bar" + x) => "foobar" + x
|
||||||
if (self.left instanceof AST_Constant
|
if (self.left instanceof AST_Constant
|
||||||
&& self.right instanceof AST_Binary
|
&& self.right instanceof AST_Binary
|
||||||
&& self.right.operator == "+"
|
&& self.right.operator == "+"
|
||||||
@@ -3064,7 +3103,7 @@ merge(Compressor.prototype, {
|
|||||||
&& self.right.is_string(compressor)) {
|
&& self.right.is_string(compressor)) {
|
||||||
self = make_node(AST_Binary, self, {
|
self = make_node(AST_Binary, self, {
|
||||||
operator: "+",
|
operator: "+",
|
||||||
left: make_node(AST_String, null, {
|
left: make_node(AST_String, self.left, {
|
||||||
value: "" + self.left.getValue() + self.right.left.getValue(),
|
value: "" + self.left.getValue() + self.right.left.getValue(),
|
||||||
start: self.left.start,
|
start: self.left.start,
|
||||||
end: self.right.left.end
|
end: self.right.left.end
|
||||||
@@ -3072,6 +3111,7 @@ merge(Compressor.prototype, {
|
|||||||
right: self.right.right
|
right: self.right.right
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// (x + "foo") + "bar" => x + "foobar"
|
||||||
if (self.right instanceof AST_Constant
|
if (self.right instanceof AST_Constant
|
||||||
&& self.left instanceof AST_Binary
|
&& self.left instanceof AST_Binary
|
||||||
&& self.left.operator == "+"
|
&& self.left.operator == "+"
|
||||||
@@ -3080,13 +3120,14 @@ merge(Compressor.prototype, {
|
|||||||
self = make_node(AST_Binary, self, {
|
self = make_node(AST_Binary, self, {
|
||||||
operator: "+",
|
operator: "+",
|
||||||
left: self.left.left,
|
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(),
|
value: "" + self.left.right.getValue() + self.right.getValue(),
|
||||||
start: self.left.right.start,
|
start: self.left.right.start,
|
||||||
end: self.right.end
|
end: self.right.end
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// (x + "foo") + ("bar" + y) => (x + "foobar") + y
|
||||||
if (self.left instanceof AST_Binary
|
if (self.left instanceof AST_Binary
|
||||||
&& self.left.operator == "+"
|
&& self.left.operator == "+"
|
||||||
&& self.left.is_string(compressor)
|
&& self.left.is_string(compressor)
|
||||||
@@ -3100,7 +3141,7 @@ merge(Compressor.prototype, {
|
|||||||
left: make_node(AST_Binary, self.left, {
|
left: make_node(AST_Binary, self.left, {
|
||||||
operator: "+",
|
operator: "+",
|
||||||
left: self.left.left,
|
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(),
|
value: "" + self.left.right.getValue() + self.right.left.getValue(),
|
||||||
start: self.left.right.start,
|
start: self.left.right.start,
|
||||||
end: self.right.left.end
|
end: self.right.left.end
|
||||||
@@ -3109,6 +3150,122 @@ merge(Compressor.prototype, {
|
|||||||
right: self.right.right
|
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
|
// x && (y && z) ==> x && y && z
|
||||||
|
|||||||
@@ -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);"
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user