From 672cdfa57ae67aae726f5b46ea20638aa1e9ce1d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 2 Aug 2022 12:01:57 +0100 Subject: [PATCH] enhance `conditionals` (#5593) --- lib/compress.js | 183 ++++++++-------- test/compress/conditionals.js | 392 ++++++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+), 84 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e4ba897f..8e446209 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1749,7 +1749,7 @@ Compressor.prototype.compress = function(node) { } } - function needs_unbinding(compressor, val) { + function needs_unbinding(val) { return val instanceof AST_PropAccess || is_undeclared_ref(val) && val.name == "eval"; } @@ -1757,12 +1757,12 @@ Compressor.prototype.compress = function(node) { // we shouldn't compress (1,func)(something) to // func(something) because that changes the meaning of // the func (becomes lexical instead of global). - function maintain_this_binding(compressor, parent, orig, val) { + function maintain_this_binding(parent, orig, val) { var wrap = false; if (parent.TYPE == "Call") { - wrap = parent.expression === orig && needs_unbinding(compressor, val); + wrap = parent.expression === orig && needs_unbinding(val); } else if (parent instanceof AST_Template) { - wrap = parent.tag === orig && needs_unbinding(compressor, val); + wrap = parent.tag === orig && needs_unbinding(val); } else if (parent instanceof AST_UnaryPrefix) { wrap = parent.operator == "delete" || parent.operator == "typeof" && is_undeclared_ref(val); @@ -2154,7 +2154,7 @@ Compressor.prototype.compress = function(node) { var def = candidate.name.definition(); if (def.references.length - def.replaced == 1 && !compressor.exposed(def)) { def.replaced++; - return maintain_this_binding(compressor, parent, node, rvalue); + return maintain_this_binding(parent, node, rvalue); } return make_node(AST_Assign, candidate, { operator: "=", @@ -3340,7 +3340,7 @@ Compressor.prototype.compress = function(node) { function patch_sequence(node, tt) { if (node instanceof AST_Sequence) switch (node.expressions.length) { case 0: return null; - case 1: return maintain_this_binding(compressor, tt.parent(), node, node.expressions[0]); + case 1: return maintain_this_binding(tt.parent(), node, node.expressions[0]); } } @@ -7110,7 +7110,7 @@ Compressor.prototype.compress = function(node) { case 0: return List.skip; case 1: - return maintain_this_binding(compressor, parent, node, props[0].transform(tt)); + return maintain_this_binding(parent, node, props[0].transform(tt)); default: return make_sequence(node, props.map(function(prop) { return prop.transform(tt); @@ -7599,7 +7599,7 @@ Compressor.prototype.compress = function(node) { } if (node instanceof AST_Sequence) { if (node.expressions.length > 1) return; - return maintain_this_binding(compressor, tt.parent(), node, node.expressions[0]); + return maintain_this_binding(tt.parent(), node, node.expressions[0]); } }); tt.push(compressor.parent()); @@ -10316,7 +10316,7 @@ Compressor.prototype.compress = function(node) { seq.expressions.push(call); return seq.optimize(compressor); } - } else if (!needs_unbinding(compressor, exp.tail_node())) { + } else if (!needs_unbinding(exp.tail_node())) { var seq = lift_sequence_in_expression(self, compressor); if (seq !== self) return seq.optimize(compressor); } @@ -10549,7 +10549,7 @@ Compressor.prototype.compress = function(node) { && is_undeclared_ref(exp.expression) && exp.expression.name == "Object") { var call = self.clone(); - call.expression = maintain_this_binding(compressor, self, exp, exp.args[0]); + call.expression = maintain_this_binding(self, exp, exp.args[0]); return call.optimize(compressor); } } @@ -10673,7 +10673,7 @@ Compressor.prototype.compress = function(node) { if (!arg) return make_node(AST_Undefined, self); args[index] = null; var parent = this.parent(); - return parent ? maintain_this_binding(compressor, parent, node, arg) : arg; + return parent ? maintain_this_binding(parent, node, arg) : arg; } })); var save_inlined = fn.inlined; @@ -10690,7 +10690,7 @@ Compressor.prototype.compress = function(node) { exprs.push(retValue); var node = make_sequence(self, exprs).optimize(compressor); fn.inlined = save_inlined; - node = maintain_this_binding(compressor, parent, current, node); + node = maintain_this_binding(parent, current, node); if (replacing || best_of_expression(node, self) === node) { refs.forEach(function(ref) { ref.scope = exp === fn ? fn.parent_scope : exp.scope; @@ -10714,7 +10714,7 @@ Compressor.prototype.compress = function(node) { fn._squeezed = true; if (exp !== fn) fn.parent_scope = exp.scope; var node = make_sequence(self, flatten_fn()).optimize(compressor); - return maintain_this_binding(compressor, parent, current, node); + return maintain_this_binding(parent, current, node); } } if (compressor.option("side_effects") @@ -11244,7 +11244,7 @@ Compressor.prototype.compress = function(node) { merge_assignments(); trim_right_for_undefined(); if (end == 0) { - self = maintain_this_binding(compressor, compressor.parent(), compressor.self(), expressions[0]); + self = maintain_this_binding(compressor.parent(), compressor.self(), expressions[0]); if (!(self instanceof AST_Sequence)) self = self.optimize(compressor); return self; } @@ -11674,7 +11674,7 @@ Compressor.prototype.compress = function(node) { var lhs = self.left; if (lazy_op[self.operator] && !lhs.has_side_effects(compressor)) { if (lhs.equals(self.right)) { - return maintain_this_binding(compressor, parent, compressor.self(), lhs).optimize(compressor); + return maintain_this_binding(parent, compressor.self(), lhs).optimize(compressor); } mark_duplicate_condition(compressor, lhs); } @@ -11770,7 +11770,7 @@ Compressor.prototype.compress = function(node) { var ll = fuzzy_eval(compressor, self.left); if (!ll) { AST_Node.warn("Condition left of && always false [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor, parent, compressor.self(), self.left).optimize(compressor); + return maintain_this_binding(parent, compressor.self(), self.left).optimize(compressor); } else if (!(ll instanceof AST_Node)) { AST_Node.warn("Condition left of && always true [{file}:{line},{col}]", self.start); return make_sequence(self, [ self.left, self.right ]).optimize(compressor); @@ -11816,7 +11816,7 @@ Compressor.prototype.compress = function(node) { line: self.start.line, col: self.start.col, }); - return maintain_this_binding(compressor, parent, compressor.self(), self.left).optimize(compressor); + return maintain_this_binding(parent, compressor.self(), self.left).optimize(compressor); } var rr; if (!nullish && (rr = self.right.evaluate(compressor, true)) && !(rr instanceof AST_Node)) { @@ -12781,16 +12781,17 @@ Compressor.prototype.compress = function(node) { operator: self.operator.slice(0, -1), left: self.left, right: self.right, - }) : maintain_this_binding(compressor, compressor.parent(), self, self.right)).optimize(compressor); + }) : maintain_this_binding(compressor.parent(), self, self.right)).optimize(compressor); } }); OPT(AST_Conditional, function(self, compressor) { if (compressor.option("sequences") && self.condition instanceof AST_Sequence) { var expressions = self.condition.expressions.slice(); - self.condition = expressions.pop(); - expressions.push(self); - return make_sequence(self, expressions); + var node = self.clone(); + node.condition = expressions.pop(); + expressions.push(node); + return make_sequence(self, expressions).optimize(compressor); } if (!compressor.option("conditionals")) return self; var condition = self.condition; @@ -12854,16 +12855,27 @@ Compressor.prototype.compress = function(node) { right: make_node(AST_Conditional, self, { condition: condition, consequent: pop_lhs(consequent), - alternative: pop_lhs(alternative) - }) + alternative: pop_lhs(alternative), + }), }); } } + var alt_tail = alternative.tail_node(); // x ? y : y ---> x, y - if (consequent.equals(alternative)) return make_sequence(self, [ - condition, - consequent - ]).optimize(compressor); + // x ? (a, c) : (b, c) ---> x ? a : b, c + if (seq_tail.equals(alt_tail)) { + return make_sequence(self, consequent.equals(alternative) ? [ + condition, + consequent, + ] : [ + make_node(AST_Conditional, self, { + condition: condition, + consequent: pop_seq(consequent), + alternative: pop_seq(alternative), + }), + seq_tail, + ]).optimize(compressor); + } // x ? y.p : z.p ---> (x ? y : z).p // x ? y(a) : z(a) ---> (x ? y : z)(a) // x ? y.f(a) : z.f(a) ---> (x ? y : z).f(a) @@ -12893,123 +12905,111 @@ Compressor.prototype.compress = function(node) { return node; } // x ? (y ? a : b) : b ---> x && y ? a : b - if (consequent instanceof AST_Conditional - && consequent.alternative.equals(alternative)) { + if (seq_tail instanceof AST_Conditional + && seq_tail.alternative.equals(alternative)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: condition, operator: "&&", - right: consequent.condition + right: fuse(consequent, seq_tail, "condition"), }), - consequent: consequent.consequent, - alternative: alternative + consequent: seq_tail.consequent, + alternative: alternative, }); } // x ? (y ? a : b) : a ---> !x || y ? a : b - if (consequent instanceof AST_Conditional - && consequent.consequent.equals(alternative)) { + if (seq_tail instanceof AST_Conditional + && seq_tail.consequent.equals(alternative)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: negated, operator: "||", - right: consequent.condition + right: fuse(consequent, seq_tail, "condition"), }), consequent: alternative, - alternative: consequent.alternative + alternative: seq_tail.alternative, }); } // x ? a : (y ? a : b) ---> x || y ? a : b - if (alternative instanceof AST_Conditional - && consequent.equals(alternative.consequent)) { + if (alt_tail instanceof AST_Conditional + && consequent.equals(alt_tail.consequent)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: condition, operator: "||", - right: alternative.condition + right: fuse(alternative, alt_tail, "condition"), }), consequent: consequent, - alternative: alternative.alternative + alternative: alt_tail.alternative, }); } // x ? b : (y ? a : b) ---> !x && y ? a : b - if (alternative instanceof AST_Conditional - && consequent.equals(alternative.alternative)) { + if (alt_tail instanceof AST_Conditional + && consequent.equals(alt_tail.alternative)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: negated, operator: "&&", - right: alternative.condition + right: fuse(alternative, alt_tail, "condition"), }), - consequent: alternative.consequent, - alternative: consequent + consequent: alt_tail.consequent, + alternative: consequent, }); } - // x ? (a, c) : (b, c) ---> x ? a : b, c - if ((consequent instanceof AST_Sequence || alternative instanceof AST_Sequence) - && consequent.tail_node().equals(alternative.tail_node())) { - return make_sequence(self, [ - make_node(AST_Conditional, self, { - condition: condition, - consequent: pop_seq(consequent), - alternative: pop_seq(alternative) - }), - consequent.tail_node() - ]).optimize(compressor); - } // x ? y && a : a ---> (!x || y) && a - if (consequent instanceof AST_Binary - && consequent.operator == "&&" - && consequent.right.equals(alternative)) { + if (seq_tail instanceof AST_Binary + && seq_tail.operator == "&&" + && seq_tail.right.equals(alternative)) { return make_node(AST_Binary, self, { operator: "&&", left: make_node(AST_Binary, self, { operator: "||", left: negated, - right: consequent.left + right: fuse(consequent, seq_tail, "left"), }), - right: alternative + right: alternative, }).optimize(compressor); } // x ? y || a : a ---> x && y || a - if (consequent instanceof AST_Binary - && consequent.operator == "||" - && consequent.right.equals(alternative)) { + if (seq_tail instanceof AST_Binary + && seq_tail.operator == "||" + && seq_tail.right.equals(alternative)) { return make_node(AST_Binary, self, { operator: "||", left: make_node(AST_Binary, self, { operator: "&&", left: condition, - right: consequent.left + right: fuse(consequent, seq_tail, "left"), }), - right: alternative + right: alternative, }).optimize(compressor); } // x ? a : y && a ---> (x || y) && a - if (alternative instanceof AST_Binary - && alternative.operator == "&&" - && alternative.right.equals(consequent)) { + if (alt_tail instanceof AST_Binary + && alt_tail.operator == "&&" + && alt_tail.right.equals(consequent)) { return make_node(AST_Binary, self, { operator: "&&", left: make_node(AST_Binary, self, { operator: "||", left: condition, - right: alternative.left + right: fuse(alternative, alt_tail, "left"), }), - right: consequent + right: consequent, }).optimize(compressor); } // x ? a : y || a ---> !x && y || a - if (alternative instanceof AST_Binary - && alternative.operator == "||" - && alternative.right.equals(consequent)) { + if (alt_tail instanceof AST_Binary + && alt_tail.operator == "||" + && alt_tail.right.equals(consequent)) { return make_node(AST_Binary, self, { operator: "||", left: make_node(AST_Binary, self, { operator: "&&", left: negated, - right: alternative.left + right: fuse(alternative, alt_tail, "left"), }), - right: consequent + right: consequent, }).optimize(compressor); } var in_bool = compressor.option("booleans") && compressor.in_boolean_context(); @@ -13022,7 +13022,7 @@ Compressor.prototype.compress = function(node) { return make_node(AST_Binary, self, { operator: "||", left: booleanize(condition), - right: alternative + right: alternative, }); } if (is_false(consequent)) { @@ -13034,7 +13034,7 @@ Compressor.prototype.compress = function(node) { return make_node(AST_Binary, self, { operator: "&&", left: booleanize(condition.negate(compressor)), - right: alternative + right: alternative, }); } if (is_true(alternative)) { @@ -13042,7 +13042,7 @@ Compressor.prototype.compress = function(node) { return make_node(AST_Binary, self, { operator: "||", left: booleanize(condition.negate(compressor)), - right: consequent + right: consequent, }); } if (is_false(alternative)) { @@ -13050,7 +13050,7 @@ Compressor.prototype.compress = function(node) { return make_node(AST_Binary, self, { operator: "&&", left: booleanize(condition), - right: consequent + right: consequent, }); } if (compressor.option("typeofs")) mark_locally_defined(condition, consequent, alternative); @@ -13108,6 +13108,13 @@ Compressor.prototype.compress = function(node) { return -1; } + function fuse(node, tail, prop) { + if (node === tail) return tail[prop]; + var exprs = node.expressions.slice(0, -1); + exprs.push(tail[prop]); + return make_sequence(node, exprs); + } + function is_tail_equivalent(consequent, alternative) { if (consequent.TYPE != alternative.TYPE) return; if (consequent.optional != alternative.optional) return; @@ -13126,13 +13133,21 @@ Compressor.prototype.compress = function(node) { } function combine_tail(consequent, alternative, top) { - if (!is_tail_equivalent(consequent, alternative)) return !top && make_node(AST_Conditional, self, { + var seq_tail = consequent.tail_node(); + var alt_tail = alternative.tail_node(); + if (!is_tail_equivalent(seq_tail, alt_tail)) return !top && make_node(AST_Conditional, self, { condition: condition, consequent: consequent, alternative: alternative, }); - var node = consequent.clone(); - node.expression = combine_tail(consequent.expression, alternative.expression); + var node = seq_tail.clone(); + var seq_expr = fuse(consequent, seq_tail, "expression"); + var alt_expr = fuse(alternative, alt_tail, "expression"); + var combined = combine_tail(seq_expr, alt_expr); + if (seq_tail.expression instanceof AST_Sequence) { + combined = maintain_this_binding(seq_tail, seq_tail.expression, combined); + } + node.expression = combined; return node; } diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 3f6f4dc2..c0a89c3f 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -1433,6 +1433,398 @@ condition_matches_alternative: { expect_stdout: "null 0 false 5" } +condition_sequence_1: { + options = { + conditionals: true, + sequences: true, + } + input: { + function f(x, y) { + return (console.log(x), x) ? x : y; + } + console.log(f("foo", "bar")); + console.log(f(null, "baz")); + console.log(f(42)); + console.log(f()); + } + expect: { + function f(x, y) { + return console.log(x), x || y; + } + console.log(f("foo", "bar")), + console.log(f(null, "baz")), + console.log(f(42)), + console.log(f()); + } + expect_stdout: [ + "foo", + "foo", + "null", + "baz", + "42", + "42", + "undefined", + "undefined", + ] +} + +condition_sequence_2: { + options = { + conditionals: true, + sequences: true, + } + input: { + function f(x, y) { + return (console.log(y), y) ? x : y; + } + console.log(f("foo", "bar")); + console.log(f(null, "baz")); + console.log(f(42)); + console.log(f()); + } + expect: { + function f(x, y) { + return console.log(y), y && x; + } + console.log(f("foo", "bar")), + console.log(f(null, "baz")), + console.log(f(42)), + console.log(f()); + } + expect_stdout: [ + "bar", + "foo", + "baz", + "null", + "undefined", + "undefined", + "undefined", + "undefined", + ] +} + +combine_tail_sequence: { + options = { + conditionals: true, + } + input: { + var n = { + f: function() { + console.log("foo"); + return this.p; + }, + p: "FAIL 1", + }; + var o = { + f: function() { + console.log("foz"); + return this.p; + }, + p: "FAIL 2", + }; + var p = "PASS"; + function g(a) { + return a + ? (console.log("baa"), (console.log("bar"), (console.log("baz"), n).f)()) + : (console.log("moo"), (console.log("mor"), (console.log("moz"), o).f)()); + } + console.log(g()); + console.log(g(42)); + } + expect: { + var n = { + f: function() { + console.log("foo"); + return this.p; + }, + p: "FAIL 1", + }; + var o = { + f: function() { + console.log("foz"); + return this.p; + }, + p: "FAIL 2", + }; + var p = "PASS"; + function g(a) { + return (0, (a + ? (console.log("baa"), console.log("bar"), console.log("baz"), n) + : (console.log("moo"), console.log("mor"), console.log("moz"), o)).f)(); + } + console.log(g()); + console.log(g(42)); + } + expect_stdout: [ + "moo", + "mor", + "moz", + "foz", + "PASS", + "baa", + "bar", + "baz", + "foo", + "PASS", + ] +} + +consequent_sequence_1: { + options = { + conditionals: true, + } + input: { + function f(x, y, a) { + return x ? (console.log("seq"), y && a) : a; + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect: { + function f(x, y, a) { + return (!x || (console.log("seq"), y)) && a; + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect_stdout: [ + "1", + "2", + "seq", + "false", + "seq", + "4", + ] +} + +consequent_sequence_2: { + options = { + conditionals: true, + } + input: { + function f(x, y, a) { + return x ? (console.log("seq"), y || a) : a; + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect: { + function f(x, y, a) { + return x && (console.log("seq"), y) || a; + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect_stdout: [ + "1", + "2", + "seq", + "3", + "seq", + "true", + ] +} + +consequent_sequence_3: { + options = { + conditionals: true, + } + input: { + function f(x, y, a, b) { + return x ? (console.log("seq"), y ? a : b) : b; + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect: { + function f(x, y, a, b) { + return x && (console.log("seq"), y) ? a : b; + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect_stdout: [ + "-1", + "-2", + "seq", + "-3", + "seq", + "4", + ] +} + +consequent_sequence_4: { + options = { + conditionals: true, + } + input: { + function f(x, y, a, b) { + return x ? (console.log("seq"), y ? a : b) : a; + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect: { + function f(x, y, a, b) { + return !x || (console.log("seq"), y) ? a : b; + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect_stdout: [ + "1", + "2", + "seq", + "-3", + "seq", + "4", + ] +} + +alternative_sequence_1: { + options = { + conditionals: true, + } + input: { + function f(x, y, a) { + return x ? a : (console.log("seq"), y && a); + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect: { + function f(x, y, a) { + return (x || (console.log("seq"), y)) && a; + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect_stdout: [ + "seq", + "false", + "seq", + "2", + "3", + "4", + ] +} + +alternative_sequence_2: { + options = { + conditionals: true, + } + input: { + function f(x, y, a) { + return x ? a : (console.log("seq"), y || a); + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect: { + function f(x, y, a) { + return !x && (console.log("seq"), y) || a; + } + console.log(f(false, false, 1)); + console.log(f(false, true, 2)); + console.log(f(true, false, 3)); + console.log(f(true, true, 4)); + } + expect_stdout: [ + "seq", + "1", + "seq", + "true", + "3", + "4", + ] +} + +alternative_sequence_3: { + options = { + conditionals: true, + } + input: { + function f(x, y, a, b) { + return x ? a : (console.log("seq"), y ? a : b); + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect: { + function f(x, y, a, b) { + return x || (console.log("seq"), y) ? a : b; + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect_stdout: [ + "seq", + "-1", + "seq", + "2", + "3", + "4", + ] +} + +alternative_sequence_4: { + options = { + conditionals: true, + } + input: { + function f(x, y, a, b) { + return x ? b : (console.log("seq"), y ? a : b); + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect: { + function f(x, y, a, b) { + return !x && (console.log("seq"), y) ? a : b; + } + console.log(f(false, false, 1, -1)); + console.log(f(false, true, 2, -2)); + console.log(f(true, false, 3, -3)); + console.log(f(true, true, 4, -4)); + } + expect_stdout: [ + "seq", + "-1", + "seq", + "2", + "-3", + "-4", + ] +} + delete_conditional_1: { options = { booleans: true,