Skip to content

Commit 18059cc

Browse files
authoredMar 3, 2017
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
1 parent b5e0e8c commit 18059cc

File tree

3 files changed

+303
-7
lines changed

3 files changed

+303
-7
lines changed
 

‎README.md

+3
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ to set `true`; it's effectively a shortcut for `foo=true`).
350350
comparison are switching. Compression only works if both `comparisons` and
351351
`unsafe_comps` are both set to true.
352352

353+
- `unsafe_math` (default: false) -- optimize numerical expressions like
354+
`2 * x * 3` into `6 * x`, which may give imprecise floating point results.
355+
353356
- `unsafe_proto` (default: false) -- optimize expressions like
354357
`Array.prototype.slice.call(a)` into `[].slice.call(a)`
355358

‎lib/compress.js

+164-7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function Compressor(options, false_by_default) {
5454
drop_debugger : !false_by_default,
5555
unsafe : false,
5656
unsafe_comps : false,
57+
unsafe_math : false,
5758
unsafe_proto : false,
5859
conditionals : !false_by_default,
5960
comparisons : !false_by_default,
@@ -1043,6 +1044,34 @@ merge(Compressor.prototype, {
10431044
node.DEFMETHOD("is_boolean", func);
10441045
});
10451046

1047+
// methods to determine if an expression has a numeric result type
1048+
(function (def){
1049+
def(AST_Node, return_false);
1050+
def(AST_Number, return_true);
1051+
var unary = makePredicate("+ - ~ ++ --");
1052+
def(AST_Unary, function(){
1053+
return unary(this.operator);
1054+
});
1055+
var binary = makePredicate("- * / % & | ^ << >> >>>");
1056+
def(AST_Binary, function(compressor){
1057+
return binary(this.operator) || this.operator == "+"
1058+
&& this.left.is_number(compressor)
1059+
&& this.right.is_number(compressor);
1060+
});
1061+
var assign = makePredicate("-= *= /= %= &= |= ^= <<= >>= >>>=");
1062+
def(AST_Assign, function(compressor){
1063+
return assign(this.operator) || this.right.is_number(compressor);
1064+
});
1065+
def(AST_Seq, function(compressor){
1066+
return this.cdr.is_number(compressor);
1067+
});
1068+
def(AST_Conditional, function(compressor){
1069+
return this.consequent.is_number(compressor) && this.alternative.is_number(compressor);
1070+
});
1071+
})(function(node, func){
1072+
node.DEFMETHOD("is_number", func);
1073+
});
1074+
10461075
// methods to determine if an expression has a string result type
10471076
(function (def){
10481077
def(AST_Node, return_false);
@@ -2867,8 +2896,14 @@ merge(Compressor.prototype, {
28672896
right: rhs[0]
28682897
}).optimize(compressor);
28692898
}
2870-
function reverse(op, force) {
2871-
if (force || !(self.left.has_side_effects(compressor) || self.right.has_side_effects(compressor))) {
2899+
function reversible() {
2900+
return self.left instanceof AST_Constant
2901+
|| self.right instanceof AST_Constant
2902+
|| !self.left.has_side_effects(compressor)
2903+
&& !self.right.has_side_effects(compressor);
2904+
}
2905+
function reverse(op) {
2906+
if (reversible()) {
28722907
if (op) self.operator = op;
28732908
var tmp = self.left;
28742909
self.left = self.right;
@@ -2884,7 +2919,7 @@ merge(Compressor.prototype, {
28842919

28852920
if (!(self.left instanceof AST_Binary
28862921
&& PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) {
2887-
reverse(null, true);
2922+
reverse();
28882923
}
28892924
}
28902925
if (/^[!=]==?$/.test(self.operator)) {
@@ -2919,6 +2954,7 @@ merge(Compressor.prototype, {
29192954
case "===":
29202955
case "!==":
29212956
if ((self.left.is_string(compressor) && self.right.is_string(compressor)) ||
2957+
(self.left.is_number(compressor) && self.right.is_number(compressor)) ||
29222958
(self.left.is_boolean() && self.right.is_boolean())) {
29232959
self.operator = self.operator.substr(0, 2);
29242960
}
@@ -3056,22 +3092,26 @@ merge(Compressor.prototype, {
30563092
}
30573093
break;
30583094
}
3059-
if (self.operator == "+") {
3095+
var associative = true;
3096+
switch (self.operator) {
3097+
case "+":
3098+
// "foo" + ("bar" + x) => "foobar" + x
30603099
if (self.left instanceof AST_Constant
30613100
&& self.right instanceof AST_Binary
30623101
&& self.right.operator == "+"
30633102
&& self.right.left instanceof AST_Constant
30643103
&& self.right.is_string(compressor)) {
30653104
self = make_node(AST_Binary, self, {
30663105
operator: "+",
3067-
left: make_node(AST_String, null, {
3106+
left: make_node(AST_String, self.left, {
30683107
value: "" + self.left.getValue() + self.right.left.getValue(),
30693108
start: self.left.start,
30703109
end: self.right.left.end
30713110
}),
30723111
right: self.right.right
30733112
});
30743113
}
3114+
// (x + "foo") + "bar" => x + "foobar"
30753115
if (self.right instanceof AST_Constant
30763116
&& self.left instanceof AST_Binary
30773117
&& self.left.operator == "+"
@@ -3080,13 +3120,14 @@ merge(Compressor.prototype, {
30803120
self = make_node(AST_Binary, self, {
30813121
operator: "+",
30823122
left: self.left.left,
3083-
right: make_node(AST_String, null, {
3123+
right: make_node(AST_String, self.right, {
30843124
value: "" + self.left.right.getValue() + self.right.getValue(),
30853125
start: self.left.right.start,
30863126
end: self.right.end
30873127
})
30883128
});
30893129
}
3130+
// (x + "foo") + ("bar" + y) => (x + "foobar") + y
30903131
if (self.left instanceof AST_Binary
30913132
&& self.left.operator == "+"
30923133
&& self.left.is_string(compressor)
@@ -3100,7 +3141,7 @@ merge(Compressor.prototype, {
31003141
left: make_node(AST_Binary, self.left, {
31013142
operator: "+",
31023143
left: self.left.left,
3103-
right: make_node(AST_String, null, {
3144+
right: make_node(AST_String, self.left.right, {
31043145
value: "" + self.left.right.getValue() + self.right.left.getValue(),
31053146
start: self.left.right.start,
31063147
end: self.right.left.end
@@ -3109,6 +3150,122 @@ merge(Compressor.prototype, {
31093150
right: self.right.right
31103151
});
31113152
}
3153+
// a + -b => a - b
3154+
if (self.right instanceof AST_UnaryPrefix
3155+
&& self.right.operator == "-"
3156+
&& self.left.is_number(compressor)) {
3157+
self = make_node(AST_Binary, self, {
3158+
operator: "-",
3159+
left: self.left,
3160+
right: self.right.expression
3161+
});
3162+
}
3163+
// -a + b => b - a
3164+
if (self.left instanceof AST_UnaryPrefix
3165+
&& self.left.operator == "-"
3166+
&& reversible()
3167+
&& self.right.is_number(compressor)) {
3168+
self = make_node(AST_Binary, self, {
3169+
operator: "-",
3170+
left: self.right,
3171+
right: self.left.expression
3172+
});
3173+
}
3174+
case "*":
3175+
associative = compressor.option("unsafe_math");
3176+
case "&":
3177+
case "|":
3178+
case "^":
3179+
// a + +b => +b + a
3180+
if (self.left.is_number(compressor)
3181+
&& self.right.is_number(compressor)
3182+
&& reversible()
3183+
&& !(self.left instanceof AST_Binary
3184+
&& self.left.operator != self.operator
3185+
&& PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) {
3186+
var reversed = make_node(AST_Binary, self, {
3187+
operator: self.operator,
3188+
left: self.right,
3189+
right: self.left
3190+
});
3191+
if (self.right instanceof AST_Constant
3192+
&& !(self.left instanceof AST_Constant)) {
3193+
self = best_of(reversed, self);
3194+
} else {
3195+
self = best_of(self, reversed);
3196+
}
3197+
}
3198+
if (associative && self.is_number(compressor)) {
3199+
// a + (b + c) => (a + b) + c
3200+
if (self.right instanceof AST_Binary
3201+
&& self.right.operator == self.operator) {
3202+
self = make_node(AST_Binary, self, {
3203+
operator: self.operator,
3204+
left: make_node(AST_Binary, self.left, {
3205+
operator: self.operator,
3206+
left: self.left,
3207+
right: self.right.left,
3208+
start: self.left.start,
3209+
end: self.right.left.end
3210+
}),
3211+
right: self.right.right
3212+
});
3213+
}
3214+
// (n + 2) + 3 => 5 + n
3215+
// (2 * n) * 3 => 6 + n
3216+
if (self.right instanceof AST_Constant
3217+
&& self.left instanceof AST_Binary
3218+
&& self.left.operator == self.operator) {
3219+
if (self.left.left instanceof AST_Constant) {
3220+
self = make_node(AST_Binary, self, {
3221+
operator: self.operator,
3222+
left: make_node(AST_Binary, self.left, {
3223+
operator: self.operator,
3224+
left: self.left.left,
3225+
right: self.right,
3226+
start: self.left.left.start,
3227+
end: self.right.end
3228+
}),
3229+
right: self.left.right
3230+
});
3231+
} else if (self.left.right instanceof AST_Constant) {
3232+
self = make_node(AST_Binary, self, {
3233+
operator: self.operator,
3234+
left: make_node(AST_Binary, self.left, {
3235+
operator: self.operator,
3236+
left: self.left.right,
3237+
right: self.right,
3238+
start: self.left.right.start,
3239+
end: self.right.end
3240+
}),
3241+
right: self.left.left
3242+
});
3243+
}
3244+
}
3245+
// (a | 1) | (2 | d) => (3 | a) | b
3246+
if (self.left instanceof AST_Binary
3247+
&& self.left.operator == self.operator
3248+
&& self.left.right instanceof AST_Constant
3249+
&& self.right instanceof AST_Binary
3250+
&& self.right.operator == self.operator
3251+
&& self.right.left instanceof AST_Constant) {
3252+
self = make_node(AST_Binary, self, {
3253+
operator: self.operator,
3254+
left: make_node(AST_Binary, self.left, {
3255+
operator: self.operator,
3256+
left: make_node(AST_Binary, self.left.left, {
3257+
operator: self.operator,
3258+
left: self.left.right,
3259+
right: self.right.left,
3260+
start: self.left.right.start,
3261+
end: self.right.left.end
3262+
}),
3263+
right: self.left.left
3264+
}),
3265+
right: self.right.right
3266+
});
3267+
}
3268+
}
31123269
}
31133270
}
31143271
// x && (y && z) ==> x && y && z

‎test/compress/numbers.js

+136
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,139 @@ hex_numbers_in_parentheses_for_prototype_functions: {
1717
}
1818
expect_exact: "-2;(-2).toFixed(0);2;2..toFixed(0);.2;.2.toFixed(0);2e-8;2e-8.toFixed(0);0xde0b6b3a7640080;(0xde0b6b3a7640080).toFixed(0);"
1919
}
20+
21+
comparisons: {
22+
options = {
23+
comparisons: true,
24+
}
25+
input: {
26+
console.log(
27+
~x === 42,
28+
x % n === 42
29+
);
30+
}
31+
expect: {
32+
console.log(
33+
42 == ~x,
34+
x % n == 42
35+
);
36+
}
37+
}
38+
39+
evaluate_1: {
40+
options = {
41+
evaluate: true,
42+
unsafe_math: false,
43+
}
44+
input: {
45+
console.log(
46+
x + 1 + 2,
47+
x * 1 * 2,
48+
+x + 1 + 2,
49+
1 + x + 2 + 3,
50+
1 | x | 2 | 3,
51+
1 + x-- + 2 + 3,
52+
1 + (x*y + 2) + 3,
53+
1 + (2 + x + 3),
54+
1 + (2 + ~x + 3),
55+
-y + (2 + ~x + 3),
56+
1 & (2 & x & 3),
57+
1 + (2 + (x |= 0) + 3)
58+
);
59+
}
60+
expect: {
61+
console.log(
62+
x + 1 + 2,
63+
1 * x * 2,
64+
+x + 1 + 2,
65+
1 + x + 2 + 3,
66+
3 | x,
67+
1 + x-- + 2 + 3,
68+
x*y + 2 + 1 + 3,
69+
1 + (2 + x + 3),
70+
2 + ~x + 3 + 1,
71+
-y + (2 + ~x + 3),
72+
0 & x,
73+
2 + (x |= 0) + 3 + 1
74+
);
75+
}
76+
}
77+
78+
evaluate_2: {
79+
options = {
80+
evaluate: true,
81+
unsafe_math: true,
82+
}
83+
input: {
84+
console.log(
85+
x + 1 + 2,
86+
x * 1 * 2,
87+
+x + 1 + 2,
88+
1 + x + 2 + 3,
89+
1 | x | 2 | 3,
90+
1 + x-- + 2 + 3,
91+
1 + (x*y + 2) + 3,
92+
1 + (2 + x + 3),
93+
1 & (2 & x & 3),
94+
1 + (2 + (x |= 0) + 3)
95+
);
96+
}
97+
expect: {
98+
console.log(
99+
x + 1 + 2,
100+
2 * x,
101+
3 + +x,
102+
1 + x + 2 + 3,
103+
3 | x,
104+
6 + x--,
105+
6 + x*y,
106+
1 + (2 + x + 3),
107+
0 & x,
108+
6 + (x |= 0)
109+
);
110+
}
111+
}
112+
113+
evaluate_3: {
114+
options = {
115+
evaluate: true,
116+
unsafe: true,
117+
unsafe_math: true,
118+
}
119+
input: {
120+
console.log(1 + Number(x) + 2);
121+
}
122+
expect: {
123+
console.log(3 + +x);
124+
}
125+
}
126+
127+
evaluate_4: {
128+
options = {
129+
evaluate: true,
130+
}
131+
input: {
132+
console.log(
133+
1+ +a,
134+
+a+1,
135+
1+-a,
136+
-a+1,
137+
+a+ +b,
138+
+a+-b,
139+
-a+ +b,
140+
-a+-b
141+
);
142+
}
143+
expect: {
144+
console.log(
145+
+a+1,
146+
+a+1,
147+
1-a,
148+
1-a,
149+
+a+ +b,
150+
+a-b,
151+
-a+ +b,
152+
-a-b
153+
);
154+
}
155+
}

0 commit comments

Comments
 (0)
Please sign in to comment.