Skip to content

Commit

Permalink
optimize optional chain when expression will be cast to boolean (babe…
Browse files Browse the repository at this point in the history
  • Loading branch information
JLHwung authored and nicolo-ribaudo committed Dec 2, 2020
1 parent cb37570 commit ebd274f
Show file tree
Hide file tree
Showing 33 changed files with 1,611 additions and 99 deletions.
26 changes: 18 additions & 8 deletions Gulpfile.js
Expand Up @@ -102,7 +102,13 @@ const babelVersion =
function buildRollup(packages) {
const sourcemap = process.env.NODE_ENV === "production";
return Promise.all(
packages.map(async ({ src, format, dest, name, filename, version }) => {
packages.map(async ({ src, format, dest, name, filename }) => {
const pkgJSON = require("./" + src + "/package.json");
const version = pkgJSON.version + versionSuffix;
const { dependencies = {}, peerDependencies = {} } = pkgJSON;
const external = Object.keys(dependencies).concat(
Object.keys(peerDependencies)
);
let nodeResolveBrowser = false,
babelEnvName = "rollup";
switch (src) {
Expand All @@ -115,6 +121,7 @@ function buildRollup(packages) {
fancyLog(`Compiling '${chalk.cyan(input)}' with rollup ...`);
const bundle = await rollup.rollup({
input,
external,
plugins: [
rollupBabelSource(),
rollupReplace({
Expand Down Expand Up @@ -161,6 +168,7 @@ function buildRollup(packages) {
format,
name,
sourcemap: sourcemap,
exports: "named",
});

if (!process.env.IS_PUBLISH) {
Expand All @@ -180,6 +188,7 @@ function buildRollup(packages) {
format,
name,
sourcemap: sourcemap,
exports: "named",
plugins: [
rollupTerser({
// workaround https://bugs.webkit.org/show_bug.cgi?id=212725
Expand All @@ -194,13 +203,14 @@ function buildRollup(packages) {
}

const libBundles = [
{
src: "packages/babel-parser",
format: "cjs",
dest: "lib",
version: require("./packages/babel-parser/package").version + versionSuffix,
},
];
"packages/babel-parser",
"packages/babel-plugin-proposal-optional-chaining",
"packages/babel-helper-member-expression-to-functions",
].map(src => ({
src,
format: "cjs",
dest: "lib",
}));

const standaloneBundle = [
{
Expand Down
82 changes: 56 additions & 26 deletions packages/babel-helper-member-expression-to-functions/src/index.js
@@ -1,4 +1,5 @@
import * as t from "@babel/types";
import { willPathCastToBoolean } from "./util.js";

class AssignmentMemoiser {
constructor() {
Expand Down Expand Up @@ -129,6 +130,8 @@ const handle = {
return;
}

const willEndPathCastToBoolean = willPathCastToBoolean(endPath);

const rootParentPath = endPath.parentPath;
if (
rootParentPath.isUpdateExpression({ argument: node }) ||
Expand Down Expand Up @@ -238,33 +241,60 @@ const handle = {
regular = endParentPath.node;
}

replacementPath.replaceWith(
t.conditionalExpression(
t.logicalExpression(
"||",
t.binaryExpression(
"===",
baseNeedsMemoised
? t.assignmentExpression(
"=",
t.cloneNode(baseRef),
t.cloneNode(startingNode),
)
: t.cloneNode(baseRef),
t.nullLiteral(),
),
t.binaryExpression(
"===",
t.cloneNode(baseRef),
scope.buildUndefinedNode(),
),
if (willEndPathCastToBoolean) {
const nonNullishCheck = t.logicalExpression(
"&&",
t.binaryExpression(
"!==",
baseNeedsMemoised
? t.assignmentExpression(
"=",
t.cloneNode(baseRef),
t.cloneNode(startingNode),
)
: t.cloneNode(baseRef),
t.nullLiteral(),
),
isDeleteOperation
? t.booleanLiteral(true)
: scope.buildUndefinedNode(),
regular,
),
);
t.binaryExpression(
"!==",
t.cloneNode(baseRef),
scope.buildUndefinedNode(),
),
);
replacementPath.replaceWith(
t.logicalExpression("&&", nonNullishCheck, regular),
);
} else {
// todo: respect assumptions.noDocumentAll when assumptions are implemented
const nullishCheck = t.logicalExpression(
"||",
t.binaryExpression(
"===",
baseNeedsMemoised
? t.assignmentExpression(
"=",
t.cloneNode(baseRef),
t.cloneNode(startingNode),
)
: t.cloneNode(baseRef),
t.nullLiteral(),
),
t.binaryExpression(
"===",
t.cloneNode(baseRef),
scope.buildUndefinedNode(),
),
);
replacementPath.replaceWith(
t.conditionalExpression(
nullishCheck,
isDeleteOperation
? t.booleanLiteral(true)
: scope.buildUndefinedNode(),
regular,
),
);
}

// context and isDeleteOperation can not be both truthy
if (context) {
Expand Down
45 changes: 45 additions & 0 deletions packages/babel-helper-member-expression-to-functions/src/util.js
@@ -0,0 +1,45 @@
/**
* Test if a NodePath will be cast to boolean when evaluated.
*
* @example
* // returns true
* const nodePathAQDotB = NodePath("if (a?.#b) {}").get("test"); // a?.#b
* willPathCastToBoolean(nodePathAQDotB)
* @example
* // returns false
* willPathCastToBoolean(NodePath("a?.#b"))
* @todo Respect transparent expression wrappers
* @see {@link packages/babel-plugin-proposal-optional-chaining/src/util.js}
* @param {NodePath} path
* @returns {boolean}
*/
export function willPathCastToBoolean(path: NodePath): boolean {
const maybeWrapped = path;
const { node, parentPath } = maybeWrapped;
if (parentPath.isLogicalExpression()) {
const { operator, right } = parentPath.node;
if (
operator === "&&" ||
operator === "||" ||
(operator === "??" && node === right)
) {
return willPathCastToBoolean(parentPath);
}
}
if (parentPath.isSequenceExpression()) {
const { expressions } = parentPath.node;
if (expressions[expressions.length - 1] === node) {
return willPathCastToBoolean(parentPath);
} else {
// if it is in the middle of a sequence expression, we don't
// care the return value so just cast to boolean for smaller
// output
return true;
}
}
return (
parentPath.isConditional({ test: node }) ||
parentPath.isUnaryExpression({ operator: "!" }) ||
parentPath.isLoop({ test: node })
);
}
@@ -0,0 +1,119 @@
class C {
static #a = {
b: {
c: {
d: 2,
},
},
};
static testIf(o) {
if (o?.#a.b.c.d) {
return true;
}
return false;
}
static testConditional(o) {
return o?.#a.b?.c.d ? true : false;
}
static testLoop(o) {
while (o?.#a.b.c.d) {
for (; o?.#a.b.c?.d; ) {
let i = 0;
do {
i++;
if (i === 2) {
return true;
}
} while (o?.#a.b?.c.d);
}
}
return false;
}
static testNegate(o) {
return !!o?.#a.b?.c.d;
}
static testIfDeep(o) {
if (o.obj?.#a.b?.c.d) {
return true;
}
return false;
}
static testConditionalDeep(o) {
return o.obj?.#a.b?.c.d ? true : false;
}
static testLoopDeep(o) {
while (o.obj?.#a.b.c.d) {
for (; o.obj?.#a.b.c?.d; ) {
let i = 0;
do {
i++;
if (i === 2) {
return true;
}
} while (o.obj?.#a.b?.c.d);
}
}
return false;
}
static testNegateDeep(o) {
return !!o.obj?.#a.b?.c.d;
}

static testLogicalInIf(o) {
if (o?.#a.b?.c.d && o?.#a?.b.c.d) {
return true;
}
return false;
}

static testLogicalInReturn(o) {
return o?.#a.b?.c.d && o?.#a?.b.c.d;
}

static testNullishCoalescing(o) {
if (o?.#a.b?.c.non_existent ?? o?.#a.b?.c.d) {
return o?.#a.b?.c.non_existent ?? o?.#a.b?.c.d;
}
return o?.#a.b?.c.non_existent ?? o;
}

static test() {
const c = C;
expect(C.testIf(c)).toBe(true);
expect(C.testConditional(c)).toBe(true);
expect(C.testLoop(c)).toBe(true);
expect(C.testNegate(c)).toBe(true);

expect(C.testIfDeep({ obj: c })).toBe(true);
expect(C.testConditionalDeep({ obj: c })).toBe(true);
expect(C.testLoopDeep({ obj: c })).toBe(true);
expect(C.testNegateDeep({ obj: c })).toBe(true);

expect(C.testLogicalInIf(c)).toBe(true);
expect(C.testLogicalInReturn(c)).toBe(2);

expect(C.testNullishCoalescing(c)).toBe(2);
}

static testNullish() {
for (const n of [null, undefined]) {
expect(C.testIf(n)).toBe(false);
expect(C.testConditional(n)).toBe(false);
expect(C.testLoop(n)).toBe(false);
expect(C.testNegate(n)).toBe(false);

expect(C.testIfDeep({ obj: n })).toBe(false);
expect(C.testConditionalDeep({ obj: n })).toBe(false);
expect(C.testLoopDeep({ obj: n })).toBe(false);
expect(C.testNegateDeep({ obj: n })).toBe(false);

expect(C.testLogicalInIf(n)).toBe(false);
expect(C.testLogicalInReturn(n)).toBe(undefined);

expect(C.testNullishCoalescing(n)).toBe(n);
}
}
}

C.test();
C.testNullish();

0 comments on commit ebd274f

Please sign in to comment.