Skip to content

Commit

Permalink
Use Babel optional-chaining algorithm for safe expressions
Browse files Browse the repository at this point in the history
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
wcjohnson committed Sep 24, 2017
1 parent 40317d1 commit 760ec66
Show file tree
Hide file tree
Showing 27 changed files with 172 additions and 190 deletions.
13 changes: 0 additions & 13 deletions src/fixAst.lsc

This file was deleted.

48 changes: 14 additions & 34 deletions src/index.lsc
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ import { getShadowingIdentifiers, checkFalsePositiveReference } from "./variable
import { ensureConstructor, bindMethodsInConstructor, bindMethods } from "./classes";
import * as matching from "./match";
import * as comprehension from "./comprehension";
import { replaceWithSafeCall, transformSafeMemberExpression, transformExistentialExpression } from "./safe";
import { replaceWithInlinedOperator } from "./inlinedOperators";
import { transformExistentialExpression } from "./safe";
import { maybeReplaceWithInlinedOperator } from "./inlinedOperators";
import { transformForInArrayStatement, transformForInObjectStatement, lintForInArrayStatement, lintForInObjectStatement } from "./for";
import { resetHelpers } from "./helpers";
import { markIdentifier } from "./stdlib";
import { locatePluginOpts, getParserOpts, parseConfigurationDirectives } from './config'
import { getFileTypeInfo, createCompilerState, initializeCompilerState, postprocess } from './compilerState'
import { fixAst } from './fixAst'
import { transformPlaceholders } from './placeholders'
import { fixAst } from './passes/fixAst'
import { transformPlaceholders } from './passes/transformPlaceholders'
import { transformSafeExprs } from './passes/transformSafeExprs'
import { transformPipeOperator } from './pipe'

Lightscript(babel) ->
Expand Down Expand Up @@ -51,10 +52,11 @@ Lightscript(babel) ->
//// AST transforms
// Perform basic ast fixups (block bodies, etc)
fixAst(path)

// Transform placeholder expressions first.
if compilerState.opts.placeholderArgs:
transformPlaceholders(path)
// Transform safe exprs
transformSafeExprs(path)

// Main LSC transforms
path.traverse({
Expand Down Expand Up @@ -89,29 +91,9 @@ Lightscript(babel) ->
else:
comprehension.transformPlainObjectComprehension(path)

TildeCallExpression: {
// run on exit instead of enter so that SafeMemberExpression
// can process differently from a wrapping CallExpression
// eg; `a?.b~c()` -> `a == null ? null : c(a.b)`
exit(path): void ->
{ node } = path
args = [ node.left, ...node.arguments ]
callExpr = t.callExpression(node.right, args)~atNode(node)

if path~replaceWithInlinedOperator(node.right, args): return

if (node.optional):
path~replaceWithSafeCall(callExpr)
else:
path.replaceWith(callExpr)
}

CallExpression: {
exit(path): void ->
{ node } = path
if replaceWithInlinedOperator(path, node.callee, node.arguments): return
if node.optional: replaceWithSafeCall(path, node)
}
CallExpression(path): void ->
{ node } = path
maybeReplaceWithInlinedOperator(path, node.callee, node.arguments)

NamedArrowFunction(path): void ->
if (path.node.skinny):
Expand Down Expand Up @@ -190,10 +172,11 @@ Lightscript(babel) ->
exit(path): void ->
addImplicitReturns(path)

// As this is an exit visitor, other LSC transforms have reduced
// arrows to plain FunctionDeclarations by this point.
if path.node.type === "FunctionDeclaration":
// somehow this wasn't being done... may signal deeper issues...
// This is needed because named arrow declarations are new in
// Lightscript and therefore not acknowledged by Babel's default
// traversal algorithm for assessing declarations. We must
// register the declaration by hand here.
path.getFunctionParent().scope.registerDeclaration(path)
}

Expand Down Expand Up @@ -238,9 +221,6 @@ Lightscript(babel) ->
awaitExpr = t.awaitExpression(awaitIife)~allAtLoc(getLoc(path.node))
path.replaceWith(awaitExpr)

MemberExpression(path): void ->
if path.node.optional: transformSafeMemberExpression(path)

AwaitExpression(path): void ->
if (path.get("argument").isArrayExpression() || path.node.argument.type === "ArrayComprehension") {
const promiseDotAllCall = t.callExpression(
Expand Down
2 changes: 1 addition & 1 deletion src/inlinedOperators.lsc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ inlinedOperator = {
export getInlinedOperatorsEnabled(opts) ->
not ((opts.stdlib === false) or (typeof opts.stdlib === "object" && opts.stdlib.lightscript === false))

export replaceWithInlinedOperator(path, callee, args) ->
export maybeReplaceWithInlinedOperator(path, callee, args) ->
if(
!(getCompilerState().inlinedOperatorsEnabled) or
callee.type !== "Identifier"
Expand Down
20 changes: 0 additions & 20 deletions src/lscNodeTypes.lsc
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,6 @@ export registerLightscriptNodeTypes(t): void ->
}
})

if not t.hasType("TildeCallExpression"):
definePluginType("TildeCallExpression", {
visitor: ["left", "right", "arguments"],
aliases: ["CallExpression", "Expression"],
fields: {
left: {
validate: assertNodeType("Expression"),
},
right: {
validate: assertOneOf("Identifier", "MemberExpression"),
},
arguments: {
validate: chain(
assertValueType("array"),
assertEach(assertNodeType("Expression", "SpreadElement"))
),
},
},
});

if not t.hasType("NamedArrowDeclaration"):
definePluginType("NamedArrowDeclaration", {
builder: ["id", "params", "body", "skinny", "async", "generator"],
Expand Down
19 changes: 19 additions & 0 deletions src/passes/fixAst.lsc
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()
})
3 changes: 3 additions & 0 deletions src/passes/transformPlaceholders.lsc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { transformPlaceholders } from '../placeholders'

export { transformPlaceholders }
7 changes: 7 additions & 0 deletions src/passes/transformSafeExprs.lsc
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()
})
8 changes: 5 additions & 3 deletions src/ref.lsc
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import placeAtLoc from 'ast-loc-utils/lib/placeAtLoc'
import span from 'ast-loc-utils/lib/span'

export isSimple(refTarget) ->
refTarget~isa("Identifier") or refTarget~isa("ThisExpression")
refTarget~isa("Identifier") or
refTarget~isa("Super") or
refTarget~isa("ThisExpression")

// Hoist a ref up to the enclosing scope if needed.
export hoistRef(path, refTarget, varName = "ref") ->
if isSimple(refTarget):
if refTarget~isSimple():
{ ref: refTarget, assign: refTarget, isComplex: false }
else:
ref = path.scope.generateDeclaredUidIdentifier(varName)
Expand All @@ -26,7 +28,7 @@ export hoistRef(path, refTarget, varName = "ref") ->
// Create a variable declarator for a ref in the enclosing scope, if
// needed
export undeclaredRef(path, refTarget, varName = "ref") ->
if isSimple(refTarget):
if refTarget~isSimple():
{ ref: refTarget, declarator: null, isComplex: false }
else:
ref = path.scope.generateUidIdentifier(varName)
Expand Down
172 changes: 86 additions & 86 deletions src/safe.lsc
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)
)
4 changes: 0 additions & 4 deletions src/util/statementsToExpression.lsc
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ export default statementsToExpression(path, nodes: Array<Object>) ->

if t.isSequenceExpression(seqExpr):
exprs = seqExpr.expressions

if exprs.length >= 2 && path.parentPath.isExpressionStatement():
path._maybePopFromStatements(exprs)

// could be just one element due to the previous maybe popping
if exprs.length === 1: exprs[0] else: seqExpr
elif seqExpr:
Expand Down
Loading

0 comments on commit 760ec66

Please sign in to comment.