Skip to content

Commit

Permalink
Minify some Logical Expression patterns (#227)
Browse files Browse the repository at this point in the history
* Minify some Logical Expression patterns

* Memoize evaluate results, add a few more tests, upgrade jest
  • Loading branch information
boopathi authored and kangax committed Nov 3, 2016
1 parent 0f3fc78 commit fd640d1
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 2 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
"devDependencies": {
"babel-core": "^6.18.0",
"babel-jest": "^14.1.0",
"babel-jest": "^16.0.0",
"babel-plugin-transform-es2015-block-scoping": "^6.18.0",
"babel-preset-es2015": "^6.18.0",
"babel-traverse": "^6.16.0",
Expand All @@ -43,7 +43,7 @@
"gulp-babel": "^6.1.2",
"gulp-newer": "^1.1.0",
"gulp-util": "^3.0.7",
"jest-cli": "^14.1.0",
"jest-cli": "^16.0.2",
"lerna": "2.0.0-beta.26",
"lerna-changelog": "^0.2.1",
"through2": "^2.0.1",
Expand Down
157 changes: 157 additions & 0 deletions packages/babel-plugin-minify-simplify/__tests__/simplify-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2134,6 +2134,163 @@ describe("simplify-plugin", () => {
expect(transform(source)).toBe(expected);
});

// From UglifyJS
it("should simplify logical expression of the following forms of &&", () => {
// compress to right
let sources = unpad(`
a = true && foo
a = 1 && console.log("asdf")
a = 4 * 2 && foo()
a = 10 == 10 && foo() + bar()
a = "foo" && foo()
a = 1 + "a" && foo / 10
a = -1 && 5 << foo
a = 6 && 10
a = !NaN && foo()
`).split("\n");

let expected = unpad(`
a = foo;
a = console.log("asdf");
a = foo();
a = foo() + bar();
a = foo();
a = foo / 10;
a = 5 << foo;
a = 10;
a = foo();
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(expected);

// compress to left
sources = unpad(`
a = false && bar
a = NaN && console.log("a")
a = 0 && bar()
a = undefined && foo(bar)
a = 3 * 3 - 9 && bar(foo)
a = 9 == 10 && foo()
a = !"string" && foo % bar
a = 0 && 7
`).split("\n");

expected = unpad(`
a = false;
a = NaN;
a = 0;
a = undefined;
a = 0;
a = false;
a = false;
a = 0;
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(expected);

// don't compress
sources = unpad(`
a = foo() && true;
a = console.log && 3 + 8;
a = foo + bar + 5 && "a";
a = 4 << foo && -1.5;
a = bar() && false;
a = foo() && 0;
a = bar() && NaN;
a = foo() && null;
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(sources);
});

it("should simplify logical expression of the following forms of ||", () => {
// compress to left
let sources = unpad(`
a = true || condition;
a = 1 || console.log("a");
a = 2 * 3 || 2 * condition;
a = 5 == 5 || condition + 3;
a = "string" || 4 - condition;
a = 5 + "" || condition / 5;
a = -4.5 || 6 << condition;
a = 6 || 7;
`).split("\n");

let expected = unpad(`
a = true;
a = 1;
a = 6;
a = true;
a = "string";
a = "5";
a = -4.5;
a = 6;
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(expected);

sources = unpad(`
a = false || condition;
a = 0 || console.log("b");
a = NaN || console.log("c");
a = undefined || 2 * condition;
a = null || condition + 3;
a = 2 * 3 - 6 || 4 - condition;
a = 10 == 7 || condition / 5;
a = !"string" || 6 % condition;
a = null || 7;
`).split("\n");

expected = unpad(`
a = condition;
a = console.log("b");
a = console.log("c");
a = 2 * condition;
a = condition + 3;
a = 4 - condition;
a = condition / 5;
a = 6 % condition;
a = 7;
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(expected);

// don't compress
sources = unpad(`
a = condition || true;
a = console.log("a") || 2;
a = 4 - condition || "string";
a = 6 << condition || -4.5;
a = condition || false;
a = console.log("b") || NaN;
a = console.log("c") || 0;
a = 2 * condition || undefined;
a = condition + 3 || null;
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(sources);
});

it("should transform complex logical expressions", () => {
let sources = unpad(`
a = true && 1 && foo
a = 1 && 4 * 2 && console.log("asdf")
a = 4 * 2 && NaN && foo()
a = 10 == 11 || undefined && foo() + bar() && bar()
a = -1 && undefined || 5 << foo
`).split("\n");

let expected = unpad(`
a = foo;
a = console.log("asdf");
a = NaN;
a = undefined;
a = 5 << foo;
`).split("\n");

expect(sources.map((s) => transform(s))).toEqual(expected);
});

// https://github.com/babel/babili/issues/115
it("should transform impure conditional statements correctly - issue#115", () => {
const source = unpad(`
Expand Down
73 changes: 73 additions & 0 deletions packages/babel-plugin-minify-simplify/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ module.exports = ({ types: t }) => {
const or = (a, b) => t.logicalExpression("||", a, b);
const and = (a, b) => t.logicalExpression("&&", a, b);

const OP_AND = (input) => input === "&&";
const OP_OR = (input) => input === "||";

return {
name: "minify-simplify",
visitor: {
Expand Down Expand Up @@ -124,6 +127,73 @@ module.exports = ({ types: t }) => {
],
},

LogicalExpression: {
exit(path) {
// cache of path.evaluate()
const evaluateMemo = new Map;

const TRUTHY = (input) => {
// !NaN and !undefined are truthy
// separate check here as they are considered impure by babel
if (input.isUnaryExpression() && input.get("argument").isIdentifier()) {
if (input.node.argument.name === "NaN" || input.node.argument.name === "undefined") {
return true;
}
}
const evalResult = input.evaluate();
evaluateMemo.set(input, evalResult);
return evalResult.confident && input.isPure() && evalResult.value;
};

const FALSY = (input) => {
// NaN and undefined are falsy
// separate check here as they are considered impure by babel
if (input.isIdentifier()) {
if (input.node.name === "NaN" || input.node.name === "undefined") {
return true;
}
}
const evalResult = input.evaluate();
evaluateMemo.set(input, evalResult);
return evalResult.confident && input.isPure() && !evalResult.value;
};

const {
Expression: EX
} = types;

// Convention:
// [left, operator, right, handler(leftNode, rightNode)]
const matcher = new PatternMatch([
[TRUTHY, OP_AND, EX, (l, r) => r],
[FALSY, OP_AND, EX, (l) => l],
[TRUTHY, OP_OR, EX, (l) => l],
[FALSY, OP_OR, EX, (l, r) => r]
]);

const left = path.get("left");
const right = path.get("right");
const operator = path.node.operator;

const result = matcher.match(
[left, operator, right],
isPatternMatchesPath
);

if (result.match) {
// here we are sure that left.evaluate is always confident becuase
// it satisfied one of TRUTHY/FALSY paths
let value;
if (evaluateMemo.has(left)) {
value = evaluateMemo.get(left).value;
} else {
value = left.evaluate().value;
}
path.replaceWith(result.value(t.valueToNode(value), right.node));
}
}
},

ConditionalExpression: {
enter: [
// !foo ? 'foo' : 'bar' -> foo ? 'bar' : 'foo'
Expand Down Expand Up @@ -1376,6 +1446,9 @@ module.exports = ({ types: t }) => {
}
return false;
}
if (typeof patternValue === "function") {
return patternValue(inputPath);
}
if (isNodeOfType(inputPath.node, patternValue)) return true;
let evalResult = inputPath.evaluate();
if (!evalResult.confident || !inputPath.isPure()) return false;
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-plugin-minify-simplify/src/pattern-match.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ module.exports = class PatternMatch {
}
break;
}
} else {
break;
}
}
return result;
Expand Down

1 comment on commit fd640d1

@kzc
Copy link

@kzc kzc commented on fd640d1 Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boopathi I recognize many of those tests:

mishoo/UglifyJS@fedb619

It's all good but you should merge Uglify's LICENSE into babili's LICENSE file:

https://github.com/mishoo/UglifyJS2/blob/master/LICENSE

Please # to comment.