From d89f0965aac64aa6b3a08c10950ae8eba486b29e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 7 Jul 2022 05:17:23 +0100 Subject: [PATCH] enhance `conditionals` (#5542) --- lib/ast.js | 209 +++++++++++++++++++++++++++++++++- lib/compress.js | 130 +++++++++++---------- test/compress/conditionals.js | 82 +++++++++++++ test/compress/const.js | 105 +++++++++++++++++ test/compress/let.js | 90 +++++++++++++++ 5 files changed, 554 insertions(+), 62 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 8b1c127e..b82bbbb1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -109,6 +109,9 @@ var AST_Node = DEFNODE("Node", "start end", { start: "[AST_Token] The first token of this node", end: "[AST_Token] The last token of this node" }, + equals: function(node) { + return this.TYPE == node.TYPE && this._equals(node); + }, walk: function(visitor) { visitor.visit(this); }, @@ -231,6 +234,24 @@ AST_Node.disable_validation = function() { while (restore = restore_transforms.pop()) restore(); }; +function all_equals(k, l) { + return k.length == l.length && all(k, function(m, i) { + return m.equals(l[i]); + }); +} + +function list_equals(s, t) { + return s.length == t.length && all(s, function(u, i) { + return u == t[i]; + }); +} + +function prop_equals(u, v) { + if (u === v) return true; + if (u == null) return v == null; + return u instanceof AST_Node && v instanceof AST_Node && u.equals(v); +} + /* -----[ statements ]----- */ var AST_Statement = DEFNODE("Statement", null, { @@ -242,6 +263,7 @@ var AST_Statement = DEFNODE("Statement", null, { var AST_Debugger = DEFNODE("Debugger", null, { $documentation: "Represents a debugger statement", + _equals: return_true, }, AST_Statement); var AST_Directive = DEFNODE("Directive", "quote value", { @@ -250,6 +272,9 @@ var AST_Directive = DEFNODE("Directive", "quote value", { quote: "[string?] the original quote character", value: "[string] The value of this directive as a plain string (it's not an AST_String!)", }, + _equals: function(node) { + return this.value == node.value; + }, _validate: function() { if (this.quote != null) { if (typeof this.quote != "string") throw new Error("quote must be string"); @@ -260,7 +285,8 @@ var AST_Directive = DEFNODE("Directive", "quote value", { }, AST_Statement); var AST_EmptyStatement = DEFNODE("EmptyStatement", null, { - $documentation: "The empty statement (empty block or simply a semicolon)" + $documentation: "The empty statement (empty block or simply a semicolon)", + _equals: return_true, }, AST_Statement); function is_statement(node) { @@ -291,6 +317,9 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { $propdoc: { body: "[AST_Node] an expression node (should not be instanceof AST_Statement)", }, + _equals: function(node) { + return this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -342,6 +371,9 @@ var AST_Block = DEFNODE("Block", "body", { $propdoc: { body: "[AST_Statement*] an array of statements" }, + _equals: function(node) { + return all_equals(this.body, node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -376,6 +408,10 @@ var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { $propdoc: { label: "[AST_Label] a label definition" }, + _equals: function(node) { + return this.label.equals(node.label) + && this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -417,6 +453,10 @@ var AST_DWLoop = DEFNODE("DWLoop", "condition", { $propdoc: { condition: "[AST_Node] the loop condition. Should not be instanceof AST_Statement" }, + _equals: function(node) { + return this.body.equals(node.body) + && this.condition.equals(node.condition); + }, _validate: function() { if (this.TYPE == "DWLoop") throw new Error("should not instantiate AST_DWLoop"); must_be_expression(this, "condition"); @@ -431,7 +471,7 @@ var AST_Do = DEFNODE("Do", null, { node.body.walk(visitor); node.condition.walk(visitor); }); - } + }, }, AST_DWLoop); var AST_While = DEFNODE("While", null, { @@ -442,7 +482,7 @@ var AST_While = DEFNODE("While", null, { node.condition.walk(visitor); node.body.walk(visitor); }); - } + }, }, AST_DWLoop); var AST_For = DEFNODE("For", "init condition step", { @@ -452,6 +492,12 @@ var AST_For = DEFNODE("For", "init condition step", { condition: "[AST_Node?] the `for` termination clause, or null if empty", step: "[AST_Node?] the `for` update clause, or null if empty" }, + _equals: function(node) { + return prop_equals(this.init, node.init) + && prop_equals(this.condition, node.condition) + && prop_equals(this.step, node.step) + && this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -479,6 +525,11 @@ var AST_ForEnumeration = DEFNODE("ForEnumeration", "init object", { init: "[AST_Node] the assignment target during iteration", object: "[AST_Node] the object to iterate over" }, + _equals: function(node) { + return this.init.equals(node.init) + && this.object.equals(node.object) + && this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -519,6 +570,10 @@ var AST_With = DEFNODE("With", "expression", { $propdoc: { expression: "[AST_Node] the `with` expression" }, + _equals: function(node) { + return this.expression.equals(node.expression) + && this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -621,6 +676,13 @@ var AST_Lambda = DEFNODE("Lambda", "argnames length_read rest safe_ids uses_argu }); if (this.rest) this.rest.walk(tw); }, + _equals: function(node) { + return prop_equals(this.rest, node.rest) + && prop_equals(this.name, node.name) + && prop_equals(this.value, node.value) + && all_equals(this.argnames, node.argnames) + && all_equals(this.body, node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -831,6 +893,11 @@ var AST_Class = DEFNODE("Class", "extends name properties", { extends: "[AST_Node?] the super class, or null if not specified", properties: "[AST_ClassProperty*] array of class properties", }, + _equals: function(node) { + return prop_equals(this.name, node.name) + && prop_equals(this.extends, node.extends) + && all_equals(this.properties, node.properties); + }, resolve: function(def_class) { return def_class ? this : this.parent_scope.resolve(); }, @@ -883,6 +950,12 @@ var AST_ClassProperty = DEFNODE("ClassProperty", "key private static value", { static: "[boolean] whether this is a static property", value: "[AST_Node?] property value (AST_Accessor for getters/setters, AST_LambdaExpression for methods, null if not specified for fields)", }, + _equals: function(node) { + return !this.private == !node.private + && !this.static == !node.static + && prop_equals(this.key, node.key) + && prop_equals(this.value, node.value); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -959,6 +1032,9 @@ var AST_Exit = DEFNODE("Exit", "value", { $propdoc: { value: "[AST_Node?] the value returned or thrown by this statement; could be null for AST_Return" }, + _equals: function(node) { + return prop_equals(this.value, node.value); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -989,6 +1065,9 @@ var AST_LoopControl = DEFNODE("LoopControl", "label", { $propdoc: { label: "[AST_LabelRef?] the label, or null if none", }, + _equals: function(node) { + return prop_equals(this.label, node.label); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1019,6 +1098,11 @@ var AST_If = DEFNODE("If", "condition alternative", { condition: "[AST_Node] the `if` condition", alternative: "[AST_Statement?] the `else` part, or null if not present" }, + _equals: function(node) { + return this.body.equals(node.body) + && this.condition.equals(node.condition) + && prop_equals(this.alternative, node.alternative); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1116,6 +1200,10 @@ var AST_Catch = DEFNODE("Catch", "argname", { $propdoc: { argname: "[(AST_Destructured|AST_SymbolCatch)?] symbol for the exception, or null if not present", }, + _equals: function(node) { + return prop_equals(this.argname, node.argname) + && all_equals(this.body, node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1141,6 +1229,9 @@ var AST_Definitions = DEFNODE("Definitions", "definitions", { $propdoc: { definitions: "[AST_VarDef*] array of variable definitions" }, + _equals: function(node) { + return all_equals(this.definitions, node.definitions); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1197,6 +1288,10 @@ var AST_VarDef = DEFNODE("VarDef", "name value", { name: "[AST_Destructured|AST_SymbolVar] name of the variable", value: "[AST_Node?] initializer, or null of there's no initializer", }, + _equals: function(node) { + return this.name.equals(node.name) + && prop_equals(this.value, node.value); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1216,6 +1311,9 @@ var AST_ExportDeclaration = DEFNODE("ExportDeclaration", "body", { $propdoc: { body: "[AST_DefClass|AST_Definitions|AST_LambdaDefinition] the statement to export", }, + _equals: function(node) { + return this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1236,6 +1334,9 @@ var AST_ExportDefault = DEFNODE("ExportDefault", "body", { $propdoc: { body: "[AST_Node] the default export", }, + _equals: function(node) { + return this.body.equals(node.body); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1257,6 +1358,11 @@ var AST_ExportForeign = DEFNODE("ExportForeign", "aliases keys path quote", { path: "[string] the path to import module", quote: "[string?] the original quote character", }, + _equals: function(node) { + return this.path == node.path + && list_equals(this.aliases, node.aliases) + && list_equals(this.keys, node.keys); + }, _validate: function() { if (this.aliases.length != this.keys.length) { throw new Error("aliases:key length mismatch: " + this.aliases.length + " != " + this.keys.length); @@ -1280,6 +1386,9 @@ var AST_ExportReferences = DEFNODE("ExportReferences", "properties", { $propdoc: { properties: "[AST_SymbolExport*] array of aliases to export", }, + _equals: function(node) { + return all_equals(this.properties, node.properties); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1304,6 +1413,11 @@ var AST_Import = DEFNODE("Import", "all default path properties quote", { properties: "[(AST_SymbolImport*)?] array of aliases, or null if not specified", quote: "[string?] the original quote character", }, + _equals: function(node) { + return this.path == node.path + && prop_equals(this.default, node.default) + && (this.all ? prop_equals(this.all, node.all) : all_equals(this.properties, node.properties)); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1340,6 +1454,10 @@ var AST_DefaultValue = DEFNODE("DefaultValue", "name value", { name: "[AST_Destructured|AST_SymbolDeclaration] name of the variable", value: "[AST_Node] value to assign if variable is `undefined`", }, + _equals: function(node) { + return this.name.equals(node.name) + && this.value.equals(node.value); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1367,6 +1485,11 @@ var AST_Call = DEFNODE("Call", "args expression optional pure terminal", { pure: "[boolean/S] marker for side-effect-free call expression", terminal: "[boolean] whether the chain has ended", }, + _equals: function(node) { + return !this.optional == !node.optional + && this.expression.equals(node.expression) + && all_equals(this.args, node.args); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1395,6 +1518,9 @@ var AST_Sequence = DEFNODE("Sequence", "expressions", { $propdoc: { expressions: "[AST_Node*] array of expressions (at least two)" }, + _equals: function(node) { + return all_equals(this.expressions, node.expressions); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1422,6 +1548,11 @@ var AST_PropAccess = DEFNODE("PropAccess", "expression optional property termina property: "[AST_Node|string] the property to access. For AST_Dot this is always a plain string, while for AST_Sub it's an arbitrary AST_Node", terminal: "[boolean] whether the chain has ended", }, + _equals: function(node) { + return !this.optional == !node.optional + && prop_equals(this.property, node.property) + && this.expression.equals(node.expression); + }, get_property: function() { var p = this.property; if (p instanceof AST_Constant) return p.value; @@ -1466,6 +1597,9 @@ var AST_Spread = DEFNODE("Spread", "expression", { $propdoc: { expression: "[AST_Node] expression to be expanded", }, + _equals: function(node) { + return this.expression.equals(node.expression); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1483,6 +1617,10 @@ var AST_Unary = DEFNODE("Unary", "operator expression", { operator: "[string] the operator", expression: "[AST_Node] expression that this unary operator applies to" }, + _equals: function(node) { + return this.operator == node.operator + && this.expression.equals(node.expression); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1511,6 +1649,11 @@ var AST_Binary = DEFNODE("Binary", "operator left right", { operator: "[string] the operator", right: "[AST_Node] right-hand side expression" }, + _equals: function(node) { + return this.operator == node.operator + && this.left.equals(node.left) + && this.right.equals(node.right); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1532,6 +1675,11 @@ var AST_Conditional = DEFNODE("Conditional", "condition consequent alternative", consequent: "[AST_Node]", alternative: "[AST_Node]" }, + _equals: function(node) { + return this.condition.equals(node.condition) + && this.consequent.equals(node.consequent) + && this.alternative.equals(node.alternative); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1573,6 +1721,9 @@ var AST_Await = DEFNODE("Await", "expression", { $propdoc: { expression: "[AST_Node] expression with Promise to resolve on", }, + _equals: function(node) { + return this.expression.equals(node.expression); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1590,6 +1741,10 @@ var AST_Yield = DEFNODE("Yield", "expression nested", { expression: "[AST_Node?] return value for iterator, or null if undefined", nested: "[boolean] whether to iterate over expression as generator", }, + _equals: function(node) { + return !this.nested == !node.nested + && prop_equals(this.expression, node.expression); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1612,6 +1767,9 @@ var AST_Array = DEFNODE("Array", "elements", { $propdoc: { elements: "[AST_Node*] array of elements" }, + _equals: function(node) { + return all_equals(this.elements, node.elements); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1654,6 +1812,10 @@ var AST_DestructuredArray = DEFNODE("DestructuredArray", "elements", { $propdoc: { elements: "[(AST_DefaultValue|AST_Destructured|AST_SymbolDeclaration|AST_SymbolRef)*] array of elements", }, + _equals: function(node) { + return prop_equals(this.rest, node.rest) + && all_equals(this.elements, node.elements); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1671,6 +1833,10 @@ var AST_DestructuredKeyVal = DEFNODE("DestructuredKeyVal", "key value", { key: "[string|AST_Node] property name. For computed property this is an AST_Node.", value: "[AST_DefaultValue|AST_Destructured|AST_SymbolDeclaration|AST_SymbolRef] property value", }, + _equals: function(node) { + return prop_equals(this.key, node.key) + && this.value.equals(node.value); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1692,6 +1858,10 @@ var AST_DestructuredObject = DEFNODE("DestructuredObject", "properties", { $propdoc: { properties: "[AST_DestructuredKeyVal*] array of properties", }, + _equals: function(node) { + return prop_equals(this.rest, node.rest) + && all_equals(this.properties, node.properties); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1713,6 +1883,9 @@ var AST_Object = DEFNODE("Object", "properties", { $propdoc: { properties: "[(AST_ObjectProperty|AST_Spread)*] array of properties" }, + _equals: function(node) { + return all_equals(this.properties, node.properties); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1736,6 +1909,10 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { key: "[string|AST_Node] property name. For computed property this is an AST_Node.", value: "[AST_Node] property value. For getters and setters this is an AST_Accessor.", }, + _equals: function(node) { + return prop_equals(this.key, node.key) + && this.value.equals(node.value); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -1790,6 +1967,9 @@ var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { scope: "[AST_Scope/S] the current scope (not necessarily the definition scope)", thedef: "[SymbolDef/S] the definition of this symbol" }, + _equals: function(node) { + return this.thedef ? this.thedef === node.thedef : this.name == node.name; + }, _validate: function() { if (this.TYPE == "Symbol") throw new Error("should not instantiate AST_Symbol"); if (typeof this.name != "string") throw new Error("name must be string"); @@ -1809,6 +1989,10 @@ var AST_SymbolImport = DEFNODE("SymbolImport", "key", { $propdoc: { key: "[string] the original `export` name", }, + _equals: function(node) { + return this.name == node.name + && this.key == node.key; + }, _validate: function() { if (typeof this.key != "string") throw new Error("key must be string"); }, @@ -1866,6 +2050,10 @@ var AST_SymbolExport = DEFNODE("SymbolExport", "alias", { $propdoc: { alias: "[string] the `export` alias", }, + _equals: function(node) { + return this.name == node.name + && this.alias == node.alias; + }, _validate: function() { if (typeof this.alias != "string") throw new Error("alias must be string"); }, @@ -1877,6 +2065,7 @@ var AST_LabelRef = DEFNODE("LabelRef", null, { var AST_ObjectIdentity = DEFNODE("ObjectIdentity", null, { $documentation: "Base class for `super` & `this`", + _equals: return_true, _validate: function() { if (this.TYPE == "ObjectIdentity") throw new Error("should not instantiate AST_ObjectIdentity"); }, @@ -1911,7 +2100,12 @@ var AST_Template = DEFNODE("Template", "expressions strings tag", { $propdoc: { expressions: "[AST_Node*] the placeholder expressions", strings: "[string*] the raw text segments", - tag: "[AST_Node] tag function, or null if absent", + tag: "[AST_Node?] tag function, or null if absent", + }, + _equals: function(node) { + return prop_equals(this.tag, node.tag) + && list_equals(this.strings, node.strings) + && all_equals(this.expressions, node.expressions); }, walk: function(visitor) { var node = this; @@ -1936,6 +2130,9 @@ var AST_Template = DEFNODE("Template", "expressions strings tag", { var AST_Constant = DEFNODE("Constant", null, { $documentation: "Base class for all constants", + _equals: function(node) { + return this.value === node.value; + }, _validate: function() { if (this.TYPE == "Constant") throw new Error("should not instantiate AST_Constant"); }, @@ -1984,6 +2181,9 @@ var AST_RegExp = DEFNODE("RegExp", "value", { $propdoc: { value: "[RegExp] the actual regexp" }, + _equals: function(node) { + return "" + this.value == "" + node.value; + }, _validate: function() { if (!(this.value instanceof RegExp)) throw new Error("value must be RegExp"); }, @@ -1991,6 +2191,7 @@ var AST_RegExp = DEFNODE("RegExp", "value", { var AST_Atom = DEFNODE("Atom", null, { $documentation: "Base class for atoms", + _equals: return_true, _validate: function() { if (this.TYPE == "Atom") throw new Error("should not instantiate AST_Atom"); }, diff --git a/lib/compress.js b/lib/compress.js index 395c3618..7875dd65 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -270,10 +270,6 @@ Compressor.prototype.compress = function(node) { return self; }); - AST_Node.DEFMETHOD("equivalent_to", function(node) { - return this.TYPE == node.TYPE && this.print_to_string() == node.print_to_string(); - }); - AST_Toplevel.DEFMETHOD("hoist_exports", function(compressor) { if (!compressor.option("hoist_exports")) return; var body = this.body, props = []; @@ -930,7 +926,7 @@ Compressor.prototype.compress = function(node) { var scan = ld || left instanceof AST_Destructured; switch (node.operator) { case "=": - if (left.equivalent_to(right) && !left.has_side_effects(compressor)) { + if (left.equals(right) && !left.has_side_effects(compressor)) { right.walk(tw); walk_prop(left); node.redundant = true; @@ -2031,7 +2027,7 @@ Compressor.prototype.compress = function(node) { } // Cascade compound assignments if (compound && scan_lhs && can_replace && !stop_if_hit - && node instanceof AST_Assign && node.operator != "=" && node.left.equivalent_to(lhs)) { + && node instanceof AST_Assign && node.operator != "=" && node.left.equals(lhs)) { replaced++; changed = true; AST_Node.info("Cascading {node} [{file}:{line},{col}]", { @@ -2075,7 +2071,7 @@ Compressor.prototype.compress = function(node) { // Replace variable with assignment when found var hit_rhs; if (!(node instanceof AST_SymbolDeclaration) - && (scan_lhs && lhs.equivalent_to(node) + && (scan_lhs && lhs.equals(node) || scan_rhs && (hit_rhs = scan_rhs(node, this)))) { if (!can_replace || stop_if_hit && (hit_rhs || !lhs_local || !replace_all)) { if (!hit_rhs && !value_def) abort = true; @@ -2420,11 +2416,11 @@ Compressor.prototype.compress = function(node) { if (node !== parent.init) return true; } if (node instanceof AST_Assign) { - return node.operator != "=" && lhs.equivalent_to(node.left); + return node.operator != "=" && lhs.equals(node.left); } if (node instanceof AST_Call) { if (!(lhs instanceof AST_PropAccess)) return false; - if (!lhs.equivalent_to(node.expression)) return false; + if (!lhs.equals(node.expression)) return false; return !(rvalue instanceof AST_LambdaExpression && !rvalue.contains_this()); } if (node instanceof AST_Class) return !compressor.has_directive("use strict"); @@ -3102,7 +3098,7 @@ Compressor.prototype.compress = function(node) { return !circular && rhs_exact_match; function rhs_exact_match(node) { - return expr.equivalent_to(node); + return expr.equals(node); } } @@ -3400,7 +3396,7 @@ Compressor.prototype.compress = function(node) { var changed = false; var parent = compressor.parent(); var self = compressor.self(); - var exit, exit_defs, merge_exit; + var exit, merge_exit; var in_iife = in_lambda && parent && parent.TYPE == "Call" && parent.expression === self; var chain_if_returns = in_lambda && compressor.option("conditionals") && compressor.option("sequences"); var multiple_if_returns = has_multiple_if_returns(statements); @@ -3548,7 +3544,6 @@ Compressor.prototype.compress = function(node) { if (stat instanceof AST_Exit) { exit = stat; - exit_defs = null; continue; } @@ -3578,30 +3573,15 @@ Compressor.prototype.compress = function(node) { if (exit.TYPE != ab.TYPE) return false; var value = ab.value; if (!value) return false; - var equals = exit.equivalent_to(ab); + var equals = exit.equals(ab); if (!equals && value instanceof AST_Sequence) { value = value.tail_node(); - if (exit.value && exit.value.equivalent_to(value)) equals = 2; + if (exit.value && exit.value.equals(value)) equals = 2; } if (!equals && !exact && exit.value instanceof AST_Sequence) { - if (exit.value.tail_node().equivalent_to(value)) equals = 3; + if (exit.value.tail_node().equals(value)) equals = 3; } - if (!equals) return false; - if (exit_defs == null) { - exit_defs = new Dictionary(); - exit.walk(new TreeWalker(function(node) { - if (node instanceof AST_SymbolRef) exit_defs.set(node.name, node.definition()); - })); - if (!exit_defs.size()) exit_defs = false; - } - var abort = false; - if (exit_defs) value.walk(new TreeWalker(function(node) { - if (abort) return true; - if (node instanceof AST_SymbolRef && exit_defs.get(node.name) !== node.definition()) { - return abort = true; - } - })); - return !abort && equals; + return equals; } function can_drop_abort(ab) { @@ -4635,7 +4615,7 @@ Compressor.prototype.compress = function(node) { if (keep_unary && fixed instanceof AST_UnaryPrefix && fixed.operator == "+" - && fixed.expression.equivalent_to(this)) { + && fixed.expression.equals(this)) { return false; } this.is_number = return_false; @@ -9456,10 +9436,10 @@ Compressor.prototype.compress = function(node) { } function match(cond) { - if (node.equivalent_to(cond)) return true; + if (node.equals(cond)) return true; if (!(cond instanceof AST_UnaryPrefix)) return false; if (cond.operator != "!") return false; - if (!node.equivalent_to(cond.expression)) return false; + if (!node.equals(cond.expression)) return false; negated = true; return true; } @@ -9633,9 +9613,43 @@ Compressor.prototype.compress = function(node) { self.alternative = null; return make_node(AST_BlockStatement, self, { body: [ self, body ] }).optimize(compressor); } + if (self.alternative) { + var body_stats = as_array(self.body); + var body_index = last_index(body_stats); + var alt_stats = as_array(self.alternative); + var alt_index = last_index(alt_stats); + for (var stats = []; body_index >= 0 && alt_index >= 0;) { + var stat = body_stats[body_index]; + if (!stat.equals(alt_stats[alt_index])) break; + body_stats.splice(body_index--, 1); + alt_stats.splice(alt_index--, 1); + stats.unshift(stat); + } + if (stats.length > 0) { + self.body = body_stats.length > 0 ? make_node(AST_BlockStatement, self, { + body: body_stats, + }) : make_node(AST_EmptyStatement, self); + self.alternative = alt_stats.length > 0 ? make_node(AST_BlockStatement, self, { + body: alt_stats, + }) : null; + stats.unshift(self); + return make_node(AST_BlockStatement, self, { body: stats }).optimize(compressor); + } + } if (compressor.option("typeofs")) mark_locally_defined(self.condition, self.body, self.alternative); return self; + function as_array(node) { + return node instanceof AST_BlockStatement ? node.body : [ node ]; + } + + function last_index(stats) { + for (var index = stats.length; --index >= 0;) { + if (!is_declaration(stats[index], true)) break; + } + return index; + } + function sequencesize(stat, defuns, var_defs, refs) { if (stat == null) return []; if (stat instanceof AST_BlockStatement) { @@ -9753,7 +9767,7 @@ Compressor.prototype.compress = function(node) { case 0: var prev_block = make_node(AST_BlockStatement, prev, prev); var next_block = make_node(AST_BlockStatement, branch, { body: statements }); - if (prev_block.equivalent_to(next_block)) prev.body = []; + if (prev_block.equals(next_block)) prev.body = []; } } if (side_effects.length) { @@ -11457,7 +11471,7 @@ Compressor.prototype.compress = function(node) { if (self.left instanceof AST_SymbolRef && assign instanceof AST_Assign && assign.operator == "=" - && self.left.equivalent_to(assign.left)) { + && self.left.equals(assign.left)) { return make_node(AST_Assign, self, { operator: "=", left: assign.left, @@ -11483,7 +11497,7 @@ Compressor.prototype.compress = function(node) { 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(compressor) && self.right.is_boolean(compressor)) || - repeatable(compressor, self.left) && self.left.equivalent_to(self.right)) { + repeatable(compressor, self.left) && self.left.equals(self.right)) { self.operator = self.operator.slice(0, 2); } // XXX: intentionally falling down to the next case @@ -11530,7 +11544,7 @@ Compressor.prototype.compress = function(node) { && (is_undefined(lhs.left, compressor) && self.right.left instanceof AST_Null || lhs.left instanceof AST_Null && is_undefined(self.right.left, compressor)) && !expr.has_side_effects(compressor) - && expr.equivalent_to(self.right.right)) { + && expr.equals(self.right.right)) { lhs.operator = lhs.operator.slice(0, -1); lhs.left = make_node(AST_Null, self); return self.left; @@ -11542,7 +11556,7 @@ Compressor.prototype.compress = function(node) { if (compressor.option("booleans")) { var lhs = self.left; if (lazy_op[self.operator] && !lhs.has_side_effects(compressor)) { - if (lhs.equivalent_to(self.right)) { + if (lhs.equals(self.right)) { return maintain_this_binding(compressor, parent, compressor.self(), lhs).optimize(compressor); } mark_duplicate_condition(compressor, lhs); @@ -12494,7 +12508,7 @@ Compressor.prototype.compress = function(node) { exprs.push(self.right); return make_sequence(self, exprs).optimize(compressor); } - if (self.left.equivalent_to(self.right) && !self.left.has_side_effects(compressor)) { + if (self.left.equals(self.right) && !self.left.has_side_effects(compressor)) { return self.right; } var exp = self.left.expression; @@ -12510,7 +12524,7 @@ Compressor.prototype.compress = function(node) { } } else if (self.left instanceof AST_SymbolRef && can_drop_symbol(self.left, compressor)) { var parent; - if (self.operator == "=" && self.left.equivalent_to(self.right) + if (self.operator == "=" && self.left.equals(self.right) && !((parent = compressor.parent()) instanceof AST_UnaryPrefix && parent.operator == "delete")) { return self.right; } @@ -12697,13 +12711,13 @@ Compressor.prototype.compress = function(node) { var alternative = self.alternative; if (repeatable(compressor, condition)) { // x ? x : y ---> x || y - if (condition.equivalent_to(consequent)) return make_node(AST_Binary, self, { + if (condition.equals(consequent)) return make_node(AST_Binary, self, { operator: "||", left: condition, right: alternative, }).optimize(compressor); // x ? y : x ---> x && y - if (condition.equivalent_to(alternative)) return make_node(AST_Binary, self, { + if (condition.equals(alternative)) return make_node(AST_Binary, self, { operator: "&&", left: condition, right: consequent, @@ -12720,7 +12734,7 @@ Compressor.prototype.compress = function(node) { if ((is_eq || consequent === seq_tail) && alt_tail instanceof AST_Assign && seq_tail.operator == alt_tail.operator - && seq_tail.left.equivalent_to(alt_tail.left) + && seq_tail.left.equals(alt_tail.left) && (is_eq && seq_tail.left instanceof AST_SymbolRef || !condition.has_side_effects(compressor) && can_shift_lhs_of_tail(consequent) @@ -12737,7 +12751,7 @@ Compressor.prototype.compress = function(node) { } } // x ? y : y ---> x, y - if (consequent.equivalent_to(alternative)) return make_sequence(self, [ + if (consequent.equals(alternative)) return make_sequence(self, [ condition, consequent ]).optimize(compressor); @@ -12751,7 +12765,7 @@ Compressor.prototype.compress = function(node) { if (consequent instanceof AST_Call && alternative.TYPE == consequent.TYPE && (arg_index = arg_diff(consequent, alternative)) >= 0 - && consequent.expression.equivalent_to(alternative.expression) + && consequent.expression.equals(alternative.expression) && !condition.has_side_effects(compressor) && !consequent.expression.has_side_effects(compressor)) { var node = consequent.clone(); @@ -12771,7 +12785,7 @@ Compressor.prototype.compress = function(node) { } // x ? (y ? a : b) : b ---> x && y ? a : b if (consequent instanceof AST_Conditional - && consequent.alternative.equivalent_to(alternative)) { + && consequent.alternative.equals(alternative)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: condition, @@ -12784,7 +12798,7 @@ Compressor.prototype.compress = function(node) { } // x ? (y ? a : b) : a ---> !x || y ? a : b if (consequent instanceof AST_Conditional - && consequent.consequent.equivalent_to(alternative)) { + && consequent.consequent.equals(alternative)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: negated, @@ -12797,7 +12811,7 @@ Compressor.prototype.compress = function(node) { } // x ? a : (y ? a : b) ---> x || y ? a : b if (alternative instanceof AST_Conditional - && consequent.equivalent_to(alternative.consequent)) { + && consequent.equals(alternative.consequent)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: condition, @@ -12810,7 +12824,7 @@ Compressor.prototype.compress = function(node) { } // x ? b : (y ? a : b) ---> !x && y ? a : b if (alternative instanceof AST_Conditional - && consequent.equivalent_to(alternative.alternative)) { + && consequent.equals(alternative.alternative)) { return make_node(AST_Conditional, self, { condition: make_node(AST_Binary, self, { left: negated, @@ -12823,7 +12837,7 @@ Compressor.prototype.compress = function(node) { } // x ? (a, c) : (b, c) ---> x ? a : b, c if ((consequent instanceof AST_Sequence || alternative instanceof AST_Sequence) - && consequent.tail_node().equivalent_to(alternative.tail_node())) { + && consequent.tail_node().equals(alternative.tail_node())) { return make_sequence(self, [ make_node(AST_Conditional, self, { condition: condition, @@ -12836,7 +12850,7 @@ Compressor.prototype.compress = function(node) { // x ? y && a : a ---> (!x || y) && a if (consequent instanceof AST_Binary && consequent.operator == "&&" - && consequent.right.equivalent_to(alternative)) { + && consequent.right.equals(alternative)) { return make_node(AST_Binary, self, { operator: "&&", left: make_node(AST_Binary, self, { @@ -12850,7 +12864,7 @@ Compressor.prototype.compress = function(node) { // x ? y || a : a ---> x && y || a if (consequent instanceof AST_Binary && consequent.operator == "||" - && consequent.right.equivalent_to(alternative)) { + && consequent.right.equals(alternative)) { return make_node(AST_Binary, self, { operator: "||", left: make_node(AST_Binary, self, { @@ -12864,7 +12878,7 @@ Compressor.prototype.compress = function(node) { // x ? a : y && a ---> (x || y) && a if (alternative instanceof AST_Binary && alternative.operator == "&&" - && alternative.right.equivalent_to(consequent)) { + && alternative.right.equals(consequent)) { return make_node(AST_Binary, self, { operator: "&&", left: make_node(AST_Binary, self, { @@ -12878,7 +12892,7 @@ Compressor.prototype.compress = function(node) { // x ? a : y || a ---> !x && y || a if (alternative instanceof AST_Binary && alternative.operator == "||" - && alternative.right.equivalent_to(consequent)) { + && alternative.right.equals(consequent)) { return make_node(AST_Binary, self, { operator: "||", left: make_node(AST_Binary, self, { @@ -12974,10 +12988,10 @@ Compressor.prototype.compress = function(node) { var len = a.length; if (len != b.length) return -2; for (var i = 0; i < len; i++) { - if (!a[i].equivalent_to(b[i])) { + if (!a[i].equals(b[i])) { if (a[i] instanceof AST_Spread !== b[i] instanceof AST_Spread) return -3; for (var j = i + 1; j < len; j++) { - if (!a[j].equivalent_to(b[j])) return -2; + if (!a[j].equals(b[j])) return -2; } return i; } @@ -12998,7 +13012,7 @@ Compressor.prototype.compress = function(node) { if (!(consequent instanceof AST_PropAccess)) return; var p = consequent.property; var q = alternative.property; - return (p instanceof AST_Node ? p.equivalent_to(q) : p == q) + return (p instanceof AST_Node ? p.equals(q) : p == q) && !(consequent.expression instanceof AST_Super || alternative.expression instanceof AST_Super); } diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 89b790a1..0d6a8181 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -196,6 +196,88 @@ ifs_7: { } } +merge_tail_1: { + options = { + conditionals: true, + } + input: { + function f(a) { + var b = "foo"; + if (a) { + while (console.log("bar")); + console.log(b); + } else { + while (console.log("baz")); + console.log(b); + } + } + f(); + f(42); + } + expect: { + function f(a) { + var b = "foo"; + if (a) + while (console.log("bar")); + else + while (console.log("baz")); + console.log(b); + } + f(); + f(42); + } + expect_stdout: [ + "baz", + "foo", + "bar", + "foo", + ] +} + +merge_tail_2: { + options = { + conditionals: true, + } + input: { + function f(a) { + var b = "foo"; + if (a) { + while (console.log("bar")); + console.log(b); + } else { + c = "baz"; + while (console.log(c)); + while (console.log("bar")); + console.log(b); + var c; + } + } + f(); + f(42); + } + expect: { + function f(a) { + var b = "foo"; + if (!a) { + c = "baz"; + while (console.log(c)); + var c; + } + while (console.log("bar")); + console.log(b); + } + f(); + f(42); + } + expect_stdout: [ + "baz", + "bar", + "foo", + "bar", + "foo", + ] +} + cond_1: { options = { conditionals: true, diff --git a/test/compress/const.js b/test/compress/const.js index a8837500..6964b3da 100644 --- a/test/compress/const.js +++ b/test/compress/const.js @@ -142,6 +142,80 @@ if_dead_branch: { expect_stdout: "undefined" } +retain_tail_1: { + options = { + conditionals: true, + } + input: { + function f(a) { + var b = "foo"; + if (a) { + const b = "bar"; + while (console.log("baz")); + console.log(b); + } else { + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect: { + function f(a) { + var b = "foo"; + if (a) { + const b = "bar"; + while (console.log("baz")); + console.log(b); + } else { + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect_stdout: true +} + +retain_tail_2: { + options = { + conditionals: true, + } + input: { + function f(a) { + var b = "foo"; + if (a) { + while (console.log("bar")); + console.log(b); + } else { + const b = "baz"; + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect: { + function f(a) { + var b = "foo"; + if (a) { + while (console.log("bar")); + console.log(b); + } else { + const b = "baz"; + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect_stdout: true +} + merge_vars_1: { options = { merge_vars: true, @@ -579,6 +653,37 @@ dead_block_after_return: { expect_stdout: true } +if_return_3: { + options = { + if_return: true, + } + input: { + var a = "PASS"; + function f(b) { + if (console) { + const b = a; + return b; + } else + while (console.log("FAIL 1")); + return b; + } + console.log(f("FAIL 2")); + } + expect: { + var a = "PASS"; + function f(b) { + if (console) { + const b = a; + return b; + } else + while (console.log("FAIL 1")); + return b; + } + console.log(f("FAIL 2")); + } + expect_stdout: true +} + do_if_continue_1: { options = { if_return: true, diff --git a/test/compress/let.js b/test/compress/let.js index b8181c4b..43280ace 100644 --- a/test/compress/let.js +++ b/test/compress/let.js @@ -190,6 +190,96 @@ if_dead_branch: { node_version: ">=4" } +retain_tail_1: { + options = { + conditionals: true, + } + input: { + "use strict"; + function f(a) { + var b = "foo"; + if (a) { + let b = "bar"; + while (console.log("baz")); + console.log(b); + } else { + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect: { + "use strict"; + function f(a) { + var b = "foo"; + if (a) { + let b = "bar"; + while (console.log("baz")); + console.log(b); + } else { + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect_stdout: [ + "moo", + "foo", + "baz", + "bar", + ] + node_version: ">=4" +} + +retain_tail_2: { + options = { + conditionals: true, + } + input: { + "use strict"; + function f(a) { + var b = "foo"; + if (a) { + while (console.log("bar")); + console.log(b); + } else { + let b = "baz"; + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect: { + "use strict"; + function f(a) { + var b = "foo"; + if (a) { + while (console.log("bar")); + console.log(b); + } else { + let b = "baz"; + while (console.log("moo")); + console.log(b); + } + } + f(); + f(42); + } + expect_stdout: [ + "moo", + "baz", + "bar", + "foo", + ] + node_version: ">=4" +} + merge_vars_1: { options = { merge_vars: true,