From 257ddc3bdb37efdb48fc23371f5f523e2044afd8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 1 Apr 2017 03:02:14 +0800 Subject: [PATCH] improve compression of undefined, NaN & Infinitiy (#1748) - migrate transformation logic from `OutputStream` to `Compressor` - always turn `undefined` into `void 0` (unless `unsafe`) - always keep `NaN` except when avoiding local variable redefinition - introduce `keep_infinity` to suppress `1/0` transform, except when avoiding local variable redefinition supersedes #1723 fixes #1730 --- README.md | 3 ++ lib/compress.js | 75 +++++++++++++++++++++++++++-------- lib/output.js | 28 +------------ test/compress/conditionals.js | 8 ++-- test/compress/evaluate.js | 8 ++-- test/compress/issue-1105.js | 68 ++++++++++++++++++++++++++++++- test/compress/issue-597.js | 32 ++++++++++++++- test/compress/numbers.js | 2 +- test/compress/properties.js | 2 +- 9 files changed, 170 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 2399e23f..d57a15ce 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). integer argument larger than 1 to further reduce code size in some cases. Note: raising the number of passes will increase uglify compress time. +- `keep_infinity` -- default `false`. Pass `true` to prevent `Infinity` from + being compressed into `1/0`, which may cause performance issues on Chrome. + ### The `unsafe` option It enables some transformations that *might* break code logic in certain diff --git a/lib/compress.js b/lib/compress.js index a2332a9c..1d9cd401 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -66,6 +66,7 @@ function Compressor(options, false_by_default) { join_vars : !false_by_default, keep_fargs : true, keep_fnames : false, + keep_infinity : false, loops : !false_by_default, negate_iife : !false_by_default, passes : 1, @@ -215,7 +216,12 @@ merge(Compressor.prototype, { }) : make_node(AST_EmptyStatement, node); } return make_node(AST_SimpleStatement, node, { - body: node.value || make_node(AST_Undefined, node) + body: node.value || make_node(AST_UnaryPrefix, node, { + operator: "void", + expression: make_node(AST_Number, node, { + value: 0 + }) + }) }); } if (node instanceof AST_Lambda && node !== self) { @@ -1123,8 +1129,12 @@ merge(Compressor.prototype, { })); }; - function is_undefined(node) { - return node instanceof AST_Undefined || node.is_undefined; + function is_undefined(node, compressor) { + return node.is_undefined + || node instanceof AST_Undefined + || node instanceof AST_UnaryPrefix + && node.operator == "void" + && !node.expression.has_side_effects(compressor); } /* -----[ boolean/negation helpers ]----- */ @@ -1313,7 +1323,7 @@ merge(Compressor.prototype, { return this; } }); - var unaryPrefix = makePredicate("! ~ - +"); + var unaryPrefix = makePredicate("! ~ - + void"); AST_Node.DEFMETHOD("is_constant", function(){ // Accomodate when compress option evaluate=false // as well as the common constant expressions !0 and -1 @@ -2971,7 +2981,7 @@ merge(Compressor.prototype, { } } } - if (is_undefined(self.cdr)) { + if (is_undefined(self.cdr, compressor)) { return make_node(AST_UnaryPrefix, self, { operator : "void", expression : self.car @@ -3010,7 +3020,7 @@ merge(Compressor.prototype, { self.expression = e; return self; } else { - return make_node(AST_Undefined, self).transform(compressor); + return make_node(AST_Undefined, self).optimize(compressor); } } if (compressor.option("booleans") && compressor.in_boolean_context()) { @@ -3034,6 +3044,9 @@ merge(Compressor.prototype, { })).optimize(compressor); } } + if (self.operator == "-" && e instanceof AST_Infinity) { + e = e.transform(compressor); + } if (e instanceof AST_Binary && (self.operator == "+" || self.operator == "-") && (e.operator == "*" || e.operator == "/" || e.operator == "%")) { @@ -3043,8 +3056,7 @@ merge(Compressor.prototype, { } // avoids infinite recursion of numerals if (self.operator != "-" - || !(self.expression instanceof AST_Number - || self.expression instanceof AST_Infinity)) { + || !(e instanceof AST_Number || e instanceof AST_Infinity)) { var ev = self.evaluate(compressor); if (ev !== self) { ev = make_node_from_constant(ev, self).optimize(compressor); @@ -3087,8 +3099,8 @@ merge(Compressor.prototype, { OPT(AST_Binary, function(self, compressor){ function reversible() { - return self.left instanceof AST_Constant - || self.right instanceof AST_Constant + return self.left.is_constant() + || self.right.is_constant() || !self.left.has_side_effects(compressor) && !self.right.has_side_effects(compressor); } @@ -3101,8 +3113,8 @@ merge(Compressor.prototype, { } } if (commutativeOperators(self.operator)) { - if (self.right instanceof AST_Constant - && !(self.left instanceof AST_Constant)) { + if (self.right.is_constant() + && !self.left.is_constant()) { // if right is a constant, whatever side effects the // left side might have could not influence the // result. hence, force switch. @@ -3464,9 +3476,9 @@ merge(Compressor.prototype, { case "undefined": return make_node(AST_Undefined, self).optimize(compressor); case "NaN": - return make_node(AST_NaN, self); + return make_node(AST_NaN, self).optimize(compressor); case "Infinity": - return make_node(AST_Infinity, self); + return make_node(AST_Infinity, self).optimize(compressor); } } if (compressor.option("evaluate") && compressor.option("reduce_vars")) { @@ -3508,7 +3520,38 @@ merge(Compressor.prototype, { return ref; } } - return self; + return make_node(AST_UnaryPrefix, self, { + operator: "void", + expression: make_node(AST_Number, self, { + value: 0 + }) + }); + }); + + OPT(AST_Infinity, function(self, compressor){ + var retain = compressor.option("keep_infinity") + && !compressor.find_parent(AST_Scope).find_variable("Infinity"); + return retain ? self : make_node(AST_Binary, self, { + operator: "/", + left: make_node(AST_Number, self, { + value: 1 + }), + right: make_node(AST_Number, self, { + value: 0 + }) + }); + }); + + OPT(AST_NaN, function(self, compressor){ + return compressor.find_parent(AST_Scope).find_variable("NaN") ? make_node(AST_Binary, self, { + operator: "/", + left: make_node(AST_Number, self, { + value: 0 + }), + right: make_node(AST_Number, self, { + value: 0 + }) + }) : self; }); var ASSIGN_OPS = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; @@ -3809,7 +3852,7 @@ merge(Compressor.prototype, { OPT(AST_RegExp, literals_in_boolean_context); OPT(AST_Return, function(self, compressor){ - if (self.value && is_undefined(self.value)) { + if (self.value && is_undefined(self.value, compressor)) { self.value = null; } return self; diff --git a/lib/output.js b/lib/output.js index d5c7304a..d0adbddf 100644 --- a/lib/output.js +++ b/lib/output.js @@ -586,21 +586,12 @@ function OutputStream(options) { return first_in_statement(output); }); - PARENS([ AST_Unary, AST_Undefined ], function(output){ + PARENS(AST_Unary, function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this || p instanceof AST_Call && p.expression === this; }); - PARENS([ AST_Infinity, AST_NaN ], function(output){ - var p = output.parent(); - return p instanceof AST_PropAccess && p.expression === this - || p instanceof AST_Call && p.expression === this - || p instanceof AST_Unary && p.operator != "+" && p.operator != "-" - || p instanceof AST_Binary && p.right === this - && (p.operator == "/" || p.operator == "%"); - }); - PARENS(AST_Seq, function(output){ var p = output.parent(); return p instanceof AST_Call // (foo, bar)() or foo(1, (2, 3), 4) @@ -1258,24 +1249,7 @@ function OutputStream(options) { var def = self.definition(); output.print_name(def ? def.mangled_name || def.name : self.name); }); - DEFPRINT(AST_Undefined, function(self, output){ - output.print("void 0"); - }); DEFPRINT(AST_Hole, noop); - DEFPRINT(AST_Infinity, function(self, output){ - output.print("1"); - output.space(); - output.print("/"); - output.space(); - output.print("0"); - }); - DEFPRINT(AST_NaN, function(self, output){ - output.print("0"); - output.space(); - output.print("/"); - output.space(); - output.print("0"); - }); DEFPRINT(AST_This, function(self, output){ output.print("this"); }); diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 54d4264d..e7ea2bb2 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -840,8 +840,8 @@ equality_conditionals_false: { f(0, true, 0), f(1, 2, 3), f(1, null, 3), - f(0/0), - f(0/0, "foo"); + f(NaN), + f(NaN, "foo"); } expect_stdout: true } @@ -888,8 +888,8 @@ equality_conditionals_true: { f(0, true, 0), f(1, 2, 3), f(1, null, 3), - f(0/0), - f(0/0, "foo"); + f(NaN), + f(NaN, "foo"); } expect_stdout: true } diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 35b6b925..0d26e749 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -52,7 +52,7 @@ and: { a = 7; a = false; - a = 0/0; + a = NaN; a = 0; a = void 0; a = null; @@ -67,7 +67,7 @@ and: { a = 6 << condition && -4.5; a = condition && false; - a = console.log("b") && 0/0; + a = console.log("b") && NaN; a = console.log("c") && 0; a = 2 * condition && void 0; a = condition + 3 && null; @@ -149,7 +149,7 @@ or: { a = 6 << condition || -4.5; a = condition || false; - a = console.log("b") || 0/0; + a = console.log("b") || NaN; a = console.log("c") || 0; a = 2 * condition || void 0; a = condition + 3 || null; @@ -533,7 +533,7 @@ unsafe_array: { [1, 2, 3, a][0] + 1, 2, 3, - 0/0, + NaN, "1,21", 5, (void 0)[1] + 1 diff --git a/test/compress/issue-1105.js b/test/compress/issue-1105.js index f9412165..ea957930 100644 --- a/test/compress/issue-1105.js +++ b/test/compress/issue-1105.js @@ -193,6 +193,7 @@ assorted_Infinity_NaN_undefined_in_with_scope: { cascade: true, side_effects: true, sequences: false, + keep_infinity: false, } input: { var f = console.log; @@ -224,10 +225,73 @@ assorted_Infinity_NaN_undefined_in_with_scope: { }; if (o) { f(void 0, void 0); - f(0/0, 0/0); + f(NaN, NaN); f(1/0, 1/0); f(-1/0, -1/0); - f(0/0, 0/0); + f(NaN, NaN); + } + with (o) { + f(undefined, void 0); + f(NaN, 0/0); + f(Infinity, 1/0); + f(-Infinity, -1/0); + f(9 + undefined, 9 + void 0); + } + } + expect_stdout: true +} + +assorted_Infinity_NaN_undefined_in_with_scope_keep_infinity: { + options = { + unused: true, + evaluate: true, + dead_code: true, + conditionals: true, + comparisons: true, + booleans: true, + hoist_funs: true, + keep_fargs: true, + if_return: true, + join_vars: true, + cascade: true, + side_effects: true, + sequences: false, + keep_infinity: true, + } + input: { + var f = console.log; + var o = { + undefined : 3, + NaN : 4, + Infinity : 5, + }; + if (o) { + f(undefined, void 0); + f(NaN, 0/0); + f(Infinity, 1/0); + f(-Infinity, -(1/0)); + f(2 + 7 + undefined, 2 + 7 + void 0); + } + with (o) { + f(undefined, void 0); + f(NaN, 0/0); + f(Infinity, 1/0); + f(-Infinity, -(1/0)); + f(2 + 7 + undefined, 2 + 7 + void 0); + } + } + expect: { + var f = console.log, o = { + undefined : 3, + NaN : 4, + Infinity : 5 + }; + if (o) { + f(void 0, void 0); + f(NaN, NaN); + f(Infinity, 1/0); + f(-Infinity, -1/0); + f(NaN, NaN); } with (o) { f(undefined, void 0); diff --git a/test/compress/issue-597.js b/test/compress/issue-597.js index 987bcacc..143fcc22 100644 --- a/test/compress/issue-597.js +++ b/test/compress/issue-597.js @@ -6,7 +6,7 @@ NaN_and_Infinity_must_have_parens: { } expect: { (1/0).toString(); - (0/0).toString(); + NaN.toString(); } } @@ -24,6 +24,36 @@ NaN_and_Infinity_should_not_be_replaced_when_they_are_redefined: { } } +NaN_and_Infinity_must_have_parens_evaluate: { + options = { + evaluate: true, + } + input: { + (123456789 / 0).toString(); + (+"foo").toString(); + } + expect: { + (1/0).toString(); + NaN.toString(); + } +} + +NaN_and_Infinity_should_not_be_replaced_when_they_are_redefined_evaluate: { + options = { + evaluate: true, + } + input: { + var Infinity, NaN; + (123456789 / 0).toString(); + (+"foo").toString(); + } + expect: { + var Infinity, NaN; + (1/0).toString(); + (0/0).toString(); + } +} + beautify_off_1: { options = { evaluate: true, diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 946a7f2d..86545fba 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -186,7 +186,7 @@ unary_binary_parenthesis: { }); } expect: { - var v = [ 0, 1, 0/0, 1/0, null, void 0, true, false, "", "foo", /foo/ ]; + var v = [ 0, 1, NaN, 1/0, null, void 0, true, false, "", "foo", /foo/ ]; v.forEach(function(x) { v.forEach(function(y) { console.log( diff --git a/test/compress/properties.js b/test/compress/properties.js index 376fb9e2..29bdfe2a 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -77,7 +77,7 @@ sub_properties: { a[3.14] = 3; a.if = 4; a["foo bar"] = 5; - a[0/0] = 6; + a[NaN] = 6; a[null] = 7; a[void 0] = 8; } -- 2.34.1