From 0d7d4918eb6fb73c3cf9863479b3e66d38fad6df Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 26 Jan 2017 19:14:18 +0800 Subject: [PATCH] augment evaluate to extract within objects (#1425) - gated by `unsafe` - replaces previous optimisation specific to String.length - "123"[0] => 1 - [1, 2, 3][0] => 1 - [1, 2, 3].length => 3 - does not apply to objects with overridden prototype functions --- lib/compress.js | 66 +++++- lib/scope.js | 17 +- test/compress/arrays.js | 49 +++++ test/compress/evaluate.js | 377 +++++++++++++++++++++++++++++++++++ test/compress/properties.js | 51 ++++- test/compress/reduce_vars.js | 183 ++++++++++++++++- 6 files changed, 729 insertions(+), 14 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index bbd3659d..5c019623 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -152,6 +152,12 @@ merge(Compressor.prototype, { AST_Node.DEFMETHOD("clear_opt_flags", function(){ this.walk(new TreeWalker(function(node){ + if (node instanceof AST_SymbolRef) { + var d = node.definition(); + if (d && d.init) { + delete d.init._evaluated; + } + } if (!(node instanceof AST_Directive || node instanceof AST_Constant)) { node._squeezed = false; node._optimized = false; @@ -992,13 +998,20 @@ merge(Compressor.prototype, { // constant; otherwise it's the original or a replacement node. AST_Node.DEFMETHOD("evaluate", function(compressor){ if (!compressor.option("evaluate")) return [ this ]; + var val; try { - var val = this._eval(compressor); - return [ best_of(make_node_from_constant(compressor, val, this), this), val ]; + val = this._eval(compressor); } catch(ex) { if (ex !== def) throw ex; return [ this ]; } + var node; + try { + node = make_node_from_constant(compressor, val, this); + } catch(ex) { + return [ this ]; + } + return [ best_of(node, this), val ]; }); AST_Node.DEFMETHOD("is_constant", function(compressor){ // Accomodate when compress option evaluate=false @@ -1047,6 +1060,32 @@ merge(Compressor.prototype, { def(AST_Constant, function(){ return this.getValue(); }); + def(AST_Array, function(compressor){ + if (compressor.option("unsafe")) { + return this.elements.map(function(element) { + return ev(element, compressor); + }); + } + throw def; + }); + def(AST_Object, function(compressor){ + if (compressor.option("unsafe")) { + var val = {}; + for (var i = 0, len = this.properties.length; i < len; i++) { + var prop = this.properties[i]; + var key = prop.key; + if (key instanceof AST_Node) { + key = ev(key, compressor); + } + if (typeof Object.prototype[key] === 'function') { + throw def; + } + val[key] = ev(prop.value, compressor); + } + return val; + } + throw def; + }); def(AST_UnaryPrefix, function(compressor){ var e = this.expression; switch (this.operator) { @@ -1114,6 +1153,12 @@ merge(Compressor.prototype, { try { var d = this.definition(); if (d && (d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { + if (compressor.option("unsafe")) { + if (!HOP(d.init, '_evaluated')) { + d.init._evaluated = ev(d.init, compressor); + } + return d.init._evaluated; + } return ev(d.init, compressor); } } finally { @@ -1121,11 +1166,16 @@ merge(Compressor.prototype, { } throw def; }); - def(AST_Dot, function(compressor){ - if (compressor.option("unsafe") && this.property == "length") { - var str = ev(this.expression, compressor); - if (typeof str == "string") - return str.length; + def(AST_PropAccess, function(compressor){ + if (compressor.option("unsafe")) { + var key = this.property; + if (key instanceof AST_Node) { + key = ev(key, compressor); + } + var val = ev(this.expression, compressor); + if (val && HOP(val, key)) { + return val[key]; + } } throw def; }); @@ -2890,7 +2940,7 @@ merge(Compressor.prototype, { }); } } - return self; + return self.evaluate(compressor)[0]; }); OPT(AST_Dot, function(self, compressor){ diff --git a/lib/scope.js b/lib/scope.js index bc5db90d..ae792a0a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -184,6 +184,17 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var func = null; var globals = self.globals = new Dictionary(); var tw = new TreeWalker(function(node, descend){ + function isModified(node, level) { + var parent = tw.parent(level); + if (parent instanceof AST_Unary && (parent.operator === "++" || parent.operator === "--") + || parent instanceof AST_Assign && parent.left === node + || parent instanceof AST_Call && parent.expression === node) { + return true; + } else if (parent instanceof AST_PropAccess && parent.expression === node) { + return isModified(parent, level + 1); + } + } + if (node instanceof AST_Lambda) { var prev_func = func; func = node; @@ -197,8 +208,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolRef) { var name = node.name; - var parent = tw.parent(); - if (name == "eval" && parent instanceof AST_Call) { + if (name == "eval" && tw.parent() instanceof AST_Call) { for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) { s.uses_eval = true; } @@ -220,8 +230,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ sym = g; } node.thedef = sym; - if (parent instanceof AST_Unary && (parent.operator === "++" || parent.operator === "--") - || parent instanceof AST_Assign && parent.left === node) { + if (isModified(node, 0)) { sym.modified = true; } node.reference(); diff --git a/test/compress/arrays.js b/test/compress/arrays.js index e636347f..807ba206 100644 --- a/test/compress/arrays.js +++ b/test/compress/arrays.js @@ -72,3 +72,52 @@ constant_join_2: { var f = "strstr" + variable + "foobarmoo" + foo; } } + +for_loop: { + options = { + unsafe : true, + evaluate : true, + reduce_vars : true + }; + input: { + function f0() { + var a = [1, 2, 3]; + for (var i = 0; i < a.length; i++) { + console.log(a[i]); + } + } + + function f1() { + var a = [1, 2, 3]; + for (var i = 0, len = a.length; i < len; i++) { + console.log(a[i]); + } + } + + function f2() { + var a = [1, 2, 3]; + for (var i = 0; i < a.length; i++) { + a[i]++; + } + } + } + expect: { + function f0() { + var a = [1, 2, 3]; + for (var i = 0; i < 3; i++) + console.log(a[i]); + } + + function f1() { + var a = [1, 2, 3]; + for (var i = 0, len = 3; i < len; i++) + console.log(a[i]); + } + + function f2() { + var a = [1, 2, 3]; + for (var i = 0; i < a.length; i++) + a[i]++; + } + } +} diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index d27582f3..c74c7b24 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -37,3 +37,380 @@ positive_zero: { ); } } + +unsafe_constant: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + true.a, + false.a, + null.a, + undefined.a + ); + } + expect: { + console.log( + true.a, + false.a, + null.a, + (void 0).a + ); + } +} + +unsafe_object: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({a:1}) + 1, + ({a:1}).a + 1, + ({a:1}).b + 1, + ({a:1}).a.b + 1 + ); + } + expect: { + console.log( + ({a:1}) + 1, + 2, + ({a:1}).b + 1, + 1..b + 1 + ); + } +} + +unsafe_object_nested: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({a:{b:1}}) + 1, + ({a:{b:1}}).a + 1, + ({a:{b:1}}).b + 1, + ({a:{b:1}}).a.b + 1 + ); + } + expect: { + console.log( + ({a:{b:1}}) + 1, + ({a:{b:1}}).a + 1, + ({a:{b:1}}).b + 1, + 2 + ); + } +} + +unsafe_object_complex: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({a:{b:1},b:1}) + 1, + ({a:{b:1},b:1}).a + 1, + ({a:{b:1},b:1}).b + 1, + ({a:{b:1},b:1}).a.b + 1 + ); + } + expect: { + console.log( + ({a:{b:1},b:1}) + 1, + ({a:{b:1},b:1}).a + 1, + 2, + 2 + ); + } +} + +unsafe_object_repeated: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({a:{b:1},a:1}) + 1, + ({a:{b:1},a:1}).a + 1, + ({a:{b:1},a:1}).b + 1, + ({a:{b:1},a:1}).a.b + 1 + ); + } + expect: { + console.log( + ({a:{b:1},a:1}) + 1, + 2, + ({a:{b:1},a:1}).b + 1, + 1..b + 1 + ); + } +} + +unsafe_function: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({a:{b:1},b:function(){}}) + 1, + ({a:{b:1},b:function(){}}).a + 1, + ({a:{b:1},b:function(){}}).b + 1, + ({a:{b:1},b:function(){}}).a.b + 1 + ); + } + expect: { + console.log( + ({a:{b:1},b:function(){}}) + 1, + ({a:{b:1},b:function(){}}).a + 1, + ({a:{b:1},b:function(){}}).b + 1, + ({a:{b:1},b:function(){}}).a.b + 1 + ); + } +} + +unsafe_integer_key: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({0:1}) + 1, + ({0:1})[0] + 1, + ({0:1})["0"] + 1, + ({0:1})[1] + 1, + ({0:1})[0][1] + 1, + ({0:1})[0]["1"] + 1 + ); + } + expect: { + console.log( + ({0:1}) + 1, + 2, + 2, + ({0:1})[1] + 1, + 1[1] + 1, + 1["1"] + 1 + ); + } +} + +unsafe_integer_key_complex: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({0:{1:1},1:1}) + 1, + ({0:{1:1},1:1})[0] + 1, + ({0:{1:1},1:1})["0"] + 1, + ({0:{1:1},1:1})[1] + 1, + ({0:{1:1},1:1})[0][1] + 1, + ({0:{1:1},1:1})[0]["1"] + 1 + ); + } + expect: { + console.log( + ({0:{1:1},1:1}) + 1, + "[object Object]1", + "[object Object]1", + 2, + 2, + 2 + ); + } +} + +unsafe_float_key: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({2.72:1}) + 1, + ({2.72:1})[2.72] + 1, + ({2.72:1})["2.72"] + 1, + ({2.72:1})[3.14] + 1, + ({2.72:1})[2.72][3.14] + 1, + ({2.72:1})[2.72]["3.14"] + 1 + ); + } + expect: { + console.log( + ({2.72:1}) + 1, + 2, + 2, + ({2.72:1})[3.14] + 1, + 1[3.14] + 1, + 1["3.14"] + 1 + ); + } +} + +unsafe_float_key_complex: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + ({2.72:{3.14:1},3.14:1}) + 1, + ({2.72:{3.14:1},3.14:1})[2.72] + 1, + ({2.72:{3.14:1},3.14:1})["2.72"] + 1, + ({2.72:{3.14:1},3.14:1})[3.14] + 1, + ({2.72:{3.14:1},3.14:1})[2.72][3.14] + 1, + ({2.72:{3.14:1},3.14:1})[2.72]["3.14"] + 1 + ); + } + expect: { + console.log( + "[object Object]1", + "[object Object]1", + "[object Object]1", + 2, + 2, + 2 + ); + } +} + +unsafe_array: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + [1, , 3][1], + [1, 2, 3, a] + 1, + [1, 2, 3, 4] + 1, + [1, 2, 3, a][0] + 1, + [1, 2, 3, 4][0] + 1, + [1, 2, 3, 4][6 - 5] + 1, + [1, , 3, 4][6 - 5] + 1, + [[1, 2], [3, 4]][0] + 1, + [[1, 2], [3, 4]][6 - 5][1] + 1, + [[1, 2], , [3, 4]][6 - 5][1] + 1 + ); + } + expect: { + console.log( + void 0, + [1, 2, 3, a] + 1, + "1,2,3,41", + [1, 2, 3, a][0] + 1, + 2, + 3, + NaN, + "1,21", + 5, + (void 0)[1] + 1 + ); + } +} + +unsafe_string: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + "1234" + 1, + "1234"[0] + 1, + "1234"[6 - 5] + 1, + ("12" + "34")[0] + 1, + ("12" + "34")[6 - 5] + 1, + [1, 2, 3, 4].join("")[0] + 1 + ); + } + expect: { + console.log( + "12341", + "11", + "21", + "11", + "21", + "11" + ); + } +} + +unsafe_array_bad_index: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + [1, 2, 3, 4].a + 1, + [1, 2, 3, 4]["a"] + 1, + [1, 2, 3, 4][3.14] + 1 + ); + } + expect: { + console.log( + [1, 2, 3, 4].a + 1, + [1, 2, 3, 4]["a"] + 1, + [1, 2, 3, 4][3.14] + 1 + ); + } +} + +unsafe_string_bad_index: { + options = { + evaluate : true, + unsafe : true + } + input: { + console.log( + "1234".a + 1, + "1234"["a"] + 1, + "1234"[3.14] + 1 + ); + } + expect: { + console.log( + "1234".a + 1, + "1234"["a"] + 1, + "1234"[3.14] + 1 + ); + } +} + +unsafe_prototype_function: { + options = { + evaluate : true, + unsafe : true + } + input: { + var a = ({valueOf: 0}) < 1; + var b = ({toString: 0}) < 1; + var c = ({valueOf: 0}) + ""; + var d = ({toString: 0}) + ""; + var e = (({valueOf: 0}) + "")[2]; + var f = (({toString: 0}) + "")[2]; + var g = ({valueOf: 0}).valueOf(); + var h = ({toString: 0}).toString(); + } + expect: { + var a = ({valueOf: 0}) < 1; + var b = ({toString: 0}) < 1; + var c = ({valueOf: 0}) + ""; + var d = ({toString: 0}) + ""; + var e = (({valueOf: 0}) + "")[2]; + var f = (({toString: 0}) + "")[2]; + var g = ({valueOf: 0}).valueOf(); + var h = "" + ({toString: 0}); + } +} diff --git a/test/compress/properties.js b/test/compress/properties.js index 22f2c339..7ad54ebe 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -54,7 +54,56 @@ dot_properties_es5: { } } -evaluate_length: { +sub_properties: { + options = { + evaluate: true, + properties: true + }; + input: { + a[0] = 0; + a["0"] = 1; + a[3.14] = 2; + a["3" + ".14"] = 3; + a["i" + "f"] = 4; + a["foo" + " bar"] = 5; + a[0 / 0] = 6; + a[null] = 7; + a[undefined] = 8; + } + expect: { + a[0] = 0; + a[0] = 1; + a[3.14] = 2; + a[3.14] = 3; + a.if = 4; + a["foo bar"] = 5; + a[NaN] = 6; + a[null] = 7; + a[void 0] = 8; + } +} + +evaluate_array_length: { + options = { + properties: true, + unsafe: true, + evaluate: true + }; + input: { + a = [1, 2, 3].length; + a = [1, 2, 3].join()["len" + "gth"]; + a = [1, 2, b].length; + a = [1, 2, 3].join(b).length; + } + expect: { + a = 3; + a = 5; + a = [1, 2, b].length; + a = [1, 2, 3].join(b).length; + } +} + +evaluate_string_length: { options = { properties: true, unsafe: true, diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index a1d05012..c401ac66 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -168,4 +168,185 @@ modified: { console.log(B ? 'yes' : 'no'); } } -} \ No newline at end of file +} + +unsafe_evaluate: { + options = { + evaluate : true, + reduce_vars : true, + unsafe : true, + unused : true + } + input: { + function f0(){ + var a = { + b:1 + }; + console.log(a.b + 3); + } + + function f1(){ + var a = { + b:{ + c:1 + }, + d:2 + }; + console.log(a.b + 3, a.d + 4, a.b.c + 5, a.d.c + 6); + } + } + expect: { + function f0(){ + console.log(4); + } + + function f1(){ + var a = { + b:{ + c:1 + }, + d:2 + }; + console.log(a.b + 3, 6, 6, 2..c + 6); + } + } +} + +unsafe_evaluate_object: { + options = { + evaluate : true, + reduce_vars : true, + unsafe : true + } + input: { + function f0(){ + var a = 1; + var b = {}; + b[a] = 2; + console.log(a + 3); + } + + function f1(){ + var a = { + b:1 + }; + a.b = 2; + console.log(a.b + 3); + } + } + expect: { + function f0(){ + var a = 1; + var b = {}; + b[a] = 2; + console.log(4); + } + + function f1(){ + var a = { + b:1 + }; + a.b = 2; + console.log(a.b + 3); + } + } +} + +unsafe_evaluate_array: { + options = { + evaluate : true, + reduce_vars : true, + unsafe : true + } + input: { + function f0(){ + var a = 1; + var b = []; + b[a] = 2; + console.log(a + 3); + } + + function f1(){ + var a = [1]; + a[2] = 3; + console.log(a.length); + } + + function f2(){ + var a = [1]; + a.push(2); + console.log(a.length); + } + } + expect: { + function f0(){ + var a = 1; + var b = []; + b[a] = 2; + console.log(4); + } + + function f1(){ + var a = [1]; + a[2] = 3; + console.log(a.length); + } + + function f2(){ + var a = [1]; + a.push(2); + console.log(a.length); + } + } +} + +unsafe_evaluate_equality: { + options = { + evaluate : true, + reduce_vars : true, + unsafe : true, + unused : true + } + input: { + function f0(){ + var a = {}; + console.log(a === a); + } + + function f1(){ + var a = []; + console.log(a === a); + } + + function f2(){ + var a = {a:1, b:2}; + var b = a; + var c = a; + console.log(b === c); + } + + function f3(){ + var a = [1, 2, 3]; + var b = a; + var c = a; + console.log(b === c); + } + } + expect: { + function f0(){ + console.log(true); + } + + function f1(){ + console.log(true); + } + + function f2(){ + console.log(true); + } + + function f3(){ + console.log(true); + } + } +} -- 2.34.1