Skip to content

Commit

Permalink
optimizations of bitwise ops. Closes #1343 (#1491)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiosantoscode committed Feb 23, 2024
1 parent cf27499 commit 38debcb
Show file tree
Hide file tree
Showing 4 changed files with 435 additions and 5 deletions.
266 changes: 263 additions & 3 deletions lib/compress/index.js
Expand Up @@ -170,6 +170,7 @@ import "./drop-unused.js";
import "./reduce-vars.js";
import {
is_undeclared_ref,
bitwise_binop,
lazy_op,
is_nullish,
is_undefined,
Expand Down Expand Up @@ -373,6 +374,33 @@ class Compressor extends TreeWalker {
}
}

in_32_bit_context() {
if (!this.option("evaluate")) return false;
var self = this.self();
for (var i = 0, p; p = this.parent(i); i++) {
if (p instanceof AST_Binary && bitwise_binop.has(p.operator)) {
return true;
}
if (p instanceof AST_UnaryPrefix) {
return p.operator === "~";
}
if (
p instanceof AST_Binary
&& (
p.operator == "&&"
|| p.operator == "||"
|| p.operator == "??"
)
|| p instanceof AST_Conditional && p.condition !== self
|| p.tail_node() === self
) {
self = p;
} else {
return false;
}
}
}

get_toplevel() {
return this._toplevel;
}
Expand Down Expand Up @@ -2071,9 +2099,40 @@ def_optimize(AST_UnaryPrefix, function(self, compressor) {
right: e.right
});
}
// avoids infinite recursion of numerals
if (self.operator != "-"
|| !(e instanceof AST_Number || e instanceof AST_Infinity || e instanceof AST_BigInt)) {

if (compressor.option("evaluate")) {
// ~~x => x (in 32-bit context)
// ~~{32 bit integer} => {32 bit integer}
if (
self.operator === "~"
&& self.expression instanceof AST_UnaryPrefix
&& self.expression.operator === "~"
&& (compressor.in_32_bit_context() || self.expression.expression.is_32_bit_integer())
) {
return self.expression.expression;
}

// ~(x ^ y) => x ^ ~y
if (
self.operator === "~"
&& e instanceof AST_Binary
&& e.operator === "^"
) {
if (e.left instanceof AST_UnaryPrefix && e.left.operator === "~") {
// ~(~x ^ y) => x ^ y
e.left = e.left.bitwise_negate(true);
} else {
e.right = e.right.bitwise_negate(true);
}
return e;
}
}

if (
self.operator != "-"
// avoid infinite recursion of numerals
|| !(e instanceof AST_Number || e instanceof AST_Infinity || e instanceof AST_BigInt)
) {
var ev = self.evaluate(compressor);
if (ev !== self) {
ev = make_node_from_constant(ev, self).optimize(compressor);
Expand Down Expand Up @@ -2164,6 +2223,7 @@ def_optimize(AST_Binary, function(self, compressor) {
self.left.equivalent_to(self.right)) {
self.operator = self.operator.substr(0, 2);
}

// XXX: intentionally falling down to the next case
case "==":
case "!=":
Expand Down Expand Up @@ -2205,6 +2265,55 @@ def_optimize(AST_Binary, function(self, compressor) {
&& self.left.definition() === self.right.definition()
&& is_object(self.left.fixed_value())) {
return make_node(self.operator[0] == "=" ? AST_True : AST_False, self);
} else if (self.left.is_32_bit_integer() && self.right.is_32_bit_integer()) {
const not = node => make_node(AST_UnaryPrefix, node, {
operator: "!",
expression: node
});
const booleanify = (node, truthy) => {
if (truthy) {
return compressor.in_boolean_context()
? node
: not(not(node));
} else {
return not(node);
}
};

// The only falsy 32-bit integer is 0
if (self.left instanceof AST_Number && self.left.value === 0) {
return booleanify(self.right, self.operator[0] === "!");
}
if (self.right instanceof AST_Number && self.right.value === 0) {
return booleanify(self.left, self.operator[0] === "!");
}

// Mask all-bits check
// (x & 0xFF) != 0xFF => !(~x & 0xFF)
let and_op, x, mask;
if (
(and_op =
self.left instanceof AST_Binary ? self.left
: self.right instanceof AST_Binary ? self.right : null)
&& (mask = and_op === self.left ? self.right : self.left)
&& and_op.operator === "&"
&& mask instanceof AST_Number
&& mask.is_32_bit_integer()
&& (x =
and_op.left.equivalent_to(mask) ? and_op.right
: and_op.right.equivalent_to(mask) ? and_op.left : null)
) {
let optimized = booleanify(make_node(AST_Binary, self, {
operator: "&",
left: mask,
right: make_node(AST_UnaryPrefix, self, {
operator: "~",
expression: x
})
}), self.operator[0] === "=");

return best_of(compressor, optimized, self);
}
}
break;
case "&&":
Expand Down Expand Up @@ -2578,6 +2687,157 @@ def_optimize(AST_Binary, function(self, compressor) {
}
}
}

// bitwise ops
if (bitwise_binop.has(self.operator)) {
// Use De Morgan's laws
// z & (X | y)
// => z & X (given y & z === 0)
// => z & X | {y & z} (given y & z !== 0)
let y, z, x_node, y_node, z_node = self.left;
if (
self.operator === "&"
&& self.right instanceof AST_Binary
&& self.right.operator === "|"
&& typeof (z = self.left.evaluate(compressor)) === "number"
) {
if (typeof (y = self.right.right.evaluate(compressor)) === "number") {
// z & (X | y)
x_node = self.right.left;
y_node = self.right.right;
} else if (typeof (y = self.right.left.evaluate(compressor)) === "number") {
// z & (y | X)
x_node = self.right.right;
y_node = self.right.left;
}

if ((y & z) === 0) {
self = make_node(AST_Binary, self, {
operator: self.operator,
left: z_node,
right: x_node
});
} else {
const reordered_ops = make_node(AST_Binary, self, {
operator: "|",
left: make_node(AST_Binary, self, {
operator: "&",
left: x_node,
right: z_node
}),
right: make_node_from_constant(y & z, y_node),
});

self = best_of(compressor, self, reordered_ops);
}
}

// x ^ x => 0
// x | x => 0 | x
// x & x => 0 | x
const same_operands = self.left.equivalent_to(self.right) && !self.left.has_side_effects(compressor);
if (same_operands) {
if (self.operator === "^") {
return make_node(AST_Number, self, { value: 0 });
}
if (self.operator === "|" || self.operator === "&") {
self.left = make_node(AST_Number, self, { value: 0 });
self.operator = "|";
}
}


// Shifts that do nothing
// {anything} >> 0 => {anything} | 0
// {anything} << 0 => {anything} | 0
if (
(self.operator === "<<" || self.operator === ">>")
&& self.right instanceof AST_Number && self.right.value === 0
) {
self.operator = "|";
}

// Find useless to-bitwise conversions
// {32 bit integer} | 0 => {32 bit integer}
// {32 bit integer} ^ 0 => {32 bit integer}
const zero_side = self.right instanceof AST_Number && self.right.value === 0 ? self.right
: self.left instanceof AST_Number && self.left.value === 0 ? self.left
: null;
const non_zero_side = zero_side && (zero_side === self.right ? self.left : self.right);
if (
zero_side
&& (self.operator === "|" || self.operator === "^")
&& (non_zero_side.is_32_bit_integer() || compressor.in_32_bit_context())
) {
return non_zero_side;
}

// {anything} & 0 => 0
if (
zero_side
&& self.operator === "&"
&& !non_zero_side.has_side_effects(compressor)
) {
return zero_side;
}

const is_full_mask = (node) =>
node instanceof AST_Number && node.value === -1
||
node instanceof AST_UnaryPrefix && (
node.operator === "-"
&& node.expression instanceof AST_Number
&& node.expression.value === 1
|| node.operator === "~"
&& node.expression instanceof AST_Number
&& node.expression.value === 0);

const full_mask = is_full_mask(self.right) ? self.right
: is_full_mask(self.left) ? self.left
: null;
const non_full_mask_side = full_mask && (full_mask === self.right ? self.left : self.right);

switch (self.operator) {
case "|":
// {anything} | -1 => -1
if (full_mask && !non_full_mask_side.has_side_effects(compressor)) {
return full_mask;
}

break;
case "&":
// {32 bit integer} & -1 => {32 bit integer}
if (
full_mask
&& (non_full_mask_side.is_32_bit_integer() || compressor.in_32_bit_context())
) {
return non_full_mask_side;
}

break;
case "^":
// {anything} ^ -1 => ~{anything}
if (full_mask) {
return non_full_mask_side.bitwise_negate(compressor.in_32_bit_context());
}

// ~x ^ ~y => x ^ y
if (
self.left instanceof AST_UnaryPrefix
&& self.left.operator === "~"
&& self.right instanceof AST_UnaryPrefix
&& self.right.operator === "~"
) {
self = make_node(AST_Binary, self, {
operator: "^",
left: self.left.expression,
right: self.right.expression
});
}

break;
}
}
}
// x && (y && z) ==> x && y && z
// x || (y || z) ==> x || y || z
Expand Down
50 changes: 50 additions & 0 deletions lib/compress/inference.js
Expand Up @@ -133,6 +133,7 @@ import { pure_prop_access_globals, is_pure_native_fn, is_pure_native_method } fr
export const is_undeclared_ref = (node) =>
node instanceof AST_SymbolRef && node.definition().undeclared;

export const bitwise_binop = makePredicate("<<< >> << & | ^ ~");
export const lazy_op = makePredicate("&& || ??");
export const unary_side_effects = makePredicate("delete ++ --");

Expand Down Expand Up @@ -193,6 +194,24 @@ export const unary_side_effects = makePredicate("delete ++ --");
node.DEFMETHOD("is_number", func);
});

// methods to determine if an expression is a 32 bit integer (IE results from bitwise ops, or is an integer constant fitting in that size
(function(def_is_32_bit_integer) {
def_is_32_bit_integer(AST_Node, return_false);
def_is_32_bit_integer(AST_Number, function() {
return this.value === (this.value | 0);
});
def_is_32_bit_integer(AST_UnaryPrefix, function() {
return this.operator == "~" ? this.expression.is_number()
: this.operator === "+" ? this.expression.is_32_bit_integer()
: false;
});
def_is_32_bit_integer(AST_Binary, function() {
return bitwise_binop.has(this.operator);
});
}(function (node, func) {
node.DEFMETHOD("is_32_bit_integer", func);
}));

// methods to determine if an expression has a string result type
(function(def_is_string) {
def_is_string(AST_Node, return_false);
Expand Down Expand Up @@ -807,6 +826,37 @@ export function is_lhs(node, parent) {
});
});

(function (def_bitwise_negate) {
function basic_negation(exp) {
return make_node(AST_UnaryPrefix, exp, {
operator: "~",
expression: exp
});
}

def_bitwise_negate(AST_Node, function() {
return basic_negation(this);
});

def_bitwise_negate(AST_Number, function() {
const neg = ~this.value;
if (neg.toString().length > this.value.toString().length) {
return basic_negation(this);
}
return make_node(AST_Number, this, { value: neg });
});

def_bitwise_negate(AST_UnaryPrefix, function(in_32_bit_context) {
if (this.operator == "~" && (in_32_bit_context || this.expression.is_32_bit_integer())) {
return this.expression;
} else {
return basic_negation(this);
}
});
})(function (node, func) {
node.DEFMETHOD("bitwise_negate", func);
});

// Is the callee of this function pure?
var global_pure_fns = makePredicate("Boolean decodeURI decodeURIComponent Date encodeURI encodeURIComponent Error escape EvalError isFinite isNaN Number Object parseFloat parseInt RangeError ReferenceError String SyntaxError TypeError unescape URIError");
AST_Call.DEFMETHOD("is_callee_pure", function(compressor) {
Expand Down
4 changes: 2 additions & 2 deletions lib/size.js
Expand Up @@ -406,8 +406,8 @@ AST_Class.prototype._size = function () {
};

AST_ClassStaticBlock.prototype._size = function () {
// "class{}" + semicolons
return 7 + list_overhead(this.body);
// "static{}" + semicolons
return 8 + list_overhead(this.body);
};

AST_ClassProperty.prototype._size = function () {
Expand Down

0 comments on commit 38debcb

Please sign in to comment.