forked from lightscript/babel-plugin-lightscript
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use Babel
optional-chaining
algorithm for safe expressions
commit 187231c Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 19:22:43 2017 -0400 Misc cleanup commit cd7cdb0 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 19:11:17 2017 -0400 Move safe transforms to a separate AST visitor pass commit 832cf9d Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 17:40:19 2017 -0400 Restore unit test related to tilde chaining commit 9ac1dd4 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 17:38:48 2017 -0400 Remove old safecall code commit 0141061 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 17:34:49 2017 -0400 Correct memoization for `CallExpression` with `MemberExpression` callee commit 2766ce4 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 01:59:30 2017 -0400 Eliminate `TildeCallExpression` node type commit f2c3328 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 01:06:19 2017 -0400 Check `typeof(x) === function` in safecalls commit c39b0d1 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 00:47:48 2017 -0400 Babel safe transforms, phase 2 - Avoid memoising simple identifiers commit 83ff2d1 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 00:22:26 2017 -0400 Ref cleanup commit c592784 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 00:17:12 2017 -0400 Perform block-body fixup for optional chaining in first ast pass commit 1134d99 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sun Sep 24 00:09:41 2017 -0400 Cherry-pick block body fix from 2.3.0 commit c40d0a9 Author: William C. Johnson <wcjohnson@oigroup.net> Date: Sat Sep 23 23:48:26 2017 -0400 Preliminary babel safe transform impl
- Loading branch information
Showing
27 changed files
with
172 additions
and
190 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Perform non-semantic AST transformations like ensuring function bodies | ||
// are blocks, etc. | ||
import { isNamedArrowFunction } from '../functions' | ||
import { toBlockStatement } from '../blocks' | ||
import { isa } from '../is' | ||
import { ensureBlockArrowFunctionExpression } from '../functions' | ||
|
||
export fixAst(programPath) -> | ||
programPath.traverse({ | ||
Method(path): void -> | ||
{ node } = path | ||
if isNamedArrowFunction(node): | ||
node.body = toBlockStatement(node.body) | ||
path.replaceWith(node) | ||
|
||
MemberExpression(path): void -> | ||
if path.node.optional and path.parent~isa("ArrowFunctionExpression"): | ||
path.parentPath~ensureBlockArrowFunctionExpression() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { transformPlaceholders } from '../placeholders' | ||
|
||
export { transformPlaceholders } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { maybeTransformSafe } from '../safe' | ||
|
||
export transformSafeExprs(programPath) -> | ||
programPath.traverse({ | ||
CallExpression(path): void -> path~maybeTransformSafe() | ||
MemberExpression(path): void -> path~maybeTransformSafe() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,99 +1,99 @@ | ||
import t from './types' | ||
import atNode from 'ast-loc-utils/lib/placeAtNode' | ||
import { hoistRef } from './ref' | ||
import is, { isa } from './is' | ||
import { ensureBlockArrowFunctionExpression } from './functions' | ||
import { hoistRef, isSimple } from './ref' | ||
import { isa } from './is' | ||
|
||
import { getLoc, placeTreeAtLocWhenUnplaced as allAtLoc } from 'ast-loc-utils' | ||
|
||
export transformExistentialExpression(path) -> | ||
path.replaceWith( | ||
t.binaryExpression( | ||
"!=", | ||
path.node.argument, | ||
t.nullLiteral()~atNode(path.node) | ||
)~atNode(path.node) | ||
// Optional-replacement algorithm based on babel-plugin-transform-optional-chaining | ||
// See https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-optional-chaining/src/index.js | ||
|
||
findReplacementPath(path) -> | ||
path.find(path -> | ||
{ key, parentPath } = path | ||
if key == "left" and parentPath~isa("AssignmentExpression"): false | ||
elif key == "object" and parentPath~isa("MemberExpression"): false | ||
elif key == "callee" and (parentPath~isa("CallExpression") or parentPath~isa("NewExpression")): false | ||
elif key == 0 and path.listKey == "arguments" and path.parent.tilde: false // lift past tilde calls | ||
elif key == "argument" and parentPath~isa("UpdateExpression"): false | ||
elif key == "argument" and (parentPath~isa("UnaryExpression") and parentPath.node.operator == "delete"): false | ||
else: true | ||
) | ||
|
||
export replaceWithSafeCall(path, callExpr) -> | ||
undef = path.scope.buildUndefinedNode() | ||
let callee, typeofExpr | ||
|
||
if is("MemberExpression", callExpr.callee): | ||
memberExpr = callExpr.callee | ||
{ ref: objectRef, assign: object } = hoistRef(path, memberExpr.object, "obj") | ||
|
||
let property = memberExpr.property, propertyRef = memberExpr.property | ||
if memberExpr.computed: | ||
now { ref: propertyRef, assign: property } = hoistRef(path, memberExpr.property, "prop") | ||
|
||
now typeofExpr = t.memberExpression(object, property, memberExpr.computed) | ||
now callee = t.memberExpression(objectRef, propertyRef, memberExpr.computed) | ||
else: | ||
now { ref: callee, assign: typeofExpr } = hoistRef(path, callExpr.callee) | ||
|
||
// Generate actual safecall expr | ||
// f?(x) -> (typeof f === "function") ? f(x) : undefined | ||
path.replaceWith( | ||
t.conditionalExpression( | ||
t.binaryExpression("===", | ||
t.unaryExpression("typeof", typeofExpr), | ||
t.stringLiteral("function") | ||
), | ||
t.callExpression(callee, callExpr.arguments), | ||
undef | ||
)~allAtLoc(getLoc(path.node)) | ||
) | ||
|
||
export transformSafeMemberExpression(path) -> | ||
// x?.y -> x == null ? x : x.y | ||
// x?[y] -> x == null ? x : x[y] | ||
{ node } = path | ||
{ object } = node | ||
|
||
// Transform to vanilla member expr | ||
node.optional = false | ||
replaceOptionals(path, replacementPath): void -> | ||
{ scope } = path | ||
optionals = [] | ||
nil = scope.buildUndefinedNode() | ||
|
||
// Generate null check, hoisting ref if necessary. | ||
left = if object.type === "Identifier" or (object.type === "MemberExpression" and object.optional): | ||
object | ||
else: | ||
// "Lazy man's" fix for the `?.` fat arrow bug. | ||
// The ref hoist below would cause a bodiless ArrowExpression to gain a body with a declared ref at the top. | ||
// This invalidates the `path`, which now points to a bodiless arrow function that no longer exists in the ast. | ||
// Instead let's handle that case early. | ||
if path.parent~isa("ArrowFunctionExpression"): | ||
path.parentPath~ensureBlockArrowFunctionExpression() | ||
now path = path.parentPath.get("body.body.0.expression") | ||
// Collect all optional nodes within the local cluster of nodes | ||
let objectPath = path | ||
while objectPath~isa("MemberExpression") or objectPath~isa("CallExpression") or objectPath~isa("NewExpression"): | ||
{ node } = objectPath; | ||
if node.optional: optionals.push(node) | ||
|
||
ref = path.scope.generateDeclaredUidIdentifier("ref")~atNode(object) | ||
node.object = ref | ||
t.assignmentExpression("=", ref, object)~atNode(object) | ||
|
||
nullCheck = t.binaryExpression("==", left, t.nullLiteral()~atNode(object))~atNode(object) | ||
|
||
// Gather trailing subscripts/calls, which are parent nodes: | ||
// eg; in `o?.x.y()`, group trailing `.x.y()` into the ternary | ||
let tail = path | ||
while tail.parentPath: | ||
parent = tail.parentPath; | ||
hasChainedParent = ( | ||
parent.isMemberExpression() || | ||
(parent.isCallExpression() && parent.get("callee") === tail) || | ||
(parent.node.type === "TildeCallExpression" && parent.get("left") === tail) | ||
) | ||
|
||
if hasChainedParent: | ||
now tail = tail.parentPath | ||
if objectPath~isa("MemberExpression"): | ||
now objectPath = objectPath.get("object") | ||
else: | ||
break | ||
now objectPath = objectPath.get("callee") | ||
|
||
// Traverse optionals from innermost to outermost | ||
for let i = optionals.length - 1; i >= 0; i--: | ||
node = optionals[i] | ||
node.optional = false | ||
|
||
isCall = node~isa("CallExpression"); | ||
replaceKey = if isCall or node~isa("NewExpression"): "callee" else: "object" | ||
chain = node[replaceKey] | ||
|
||
// Memoize the expression we're chaining from. | ||
// XXX: non-idiomatic usage of let/now here is due to https://github.com/lightscript/lightscript/issues/47 | ||
// XXX: This was fixed sometime in 2.0 branch but self hosting compiler has not caught up yet. | ||
// XXX: remember to cleanup when we bring self-hosting forward | ||
let check | ||
if isCall && chain~isa("MemberExpression"): | ||
{ ref: objectRef, assign: objectAssign } = hoistRef(path, chain.object, "obj") | ||
{ ref: propertyRef, assign: propertyAssign } = if chain.computed: | ||
hoistRef(path, chain.property, "prop") | ||
else: | ||
{ ref: chain.property, assign: chain.property} | ||
chain.object = objectRef | ||
chain.property = propertyRef | ||
now check = t.memberExpression(objectAssign, propertyAssign, chain.computed) | ||
// NOTE: The babel transform sometimes generates `Function#call` here which may be more | ||
// semantically correct. | ||
else: | ||
ref = if not chain~isSimple(): scope.maybeGenerateMemoised(chain) | ||
now check = if ref: | ||
node[replaceKey] = ref | ||
t.assignmentExpression("=", ref, chain) | ||
else: | ||
chain | ||
|
||
replacementPath.replaceWith( | ||
t.conditionalExpression( | ||
if isCall: | ||
t.binaryExpression("!==", | ||
t.unaryExpression("typeof", check), | ||
t.stringLiteral("function") | ||
) | ||
else: | ||
t.binaryExpression("==", check, t.nullLiteral()) | ||
nil | ||
replacementPath.node | ||
) | ||
) | ||
|
||
undef = tail.scope.buildUndefinedNode() | ||
now replacementPath = replacementPath.get("alternate") | ||
|
||
ternary = t.conditionalExpression( | ||
nullCheck | ||
undef~atNode(tail.node) | ||
tail.node | ||
)~atNode(tail.node) | ||
export maybeTransformSafe(path): void -> | ||
if path.node.optional: | ||
replaceOptionals(path, path~findReplacementPath()) | ||
|
||
tail.replaceWith(ternary); | ||
export transformExistentialExpression(path) -> | ||
path.replaceWith( | ||
t.binaryExpression( | ||
"!=", | ||
path.node.argument, | ||
t.nullLiteral()~atNode(path.node) | ||
)~atNode(path.node) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.