Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 137 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parse } from "babylon-lightscript";
import { defaultImports, lightscriptImports, lodashImports } from "./stdlib";
import { defaultImports, lightscriptImports, lodashImports, runtimeHelpers } from "./stdlib";

export default function (babel) {
const { types: t } = babel;
Expand Down Expand Up @@ -580,6 +580,15 @@ export default function (babel) {
}
}

function collectRuntimeHelper(path, helperName) {
const programScope = path.scope.getProgramParent();
const helpers = programScope.lscRuntimeHelpers;
if (!helpers[helperName]) {
helpers[helperName] = programScope.generateUidIdentifier(helperName);
}
return helpers[helperName];
}

function makeInlineStdlibFn(inlineFnName) {
const fnId = t.identifier(inlineFnName);
const aParam = t.identifier("a");
Expand Down Expand Up @@ -608,6 +617,16 @@ export default function (babel) {
]));
}

function insertAfterImports(path, nodes) {
// insert inline fns before the first statement which isn't an import statement
for (const p of path.get("body")) {
if (!p.isImportDeclaration()) {
p.insertBefore(nodes);
break;
}
}
}

function insertStdlibImports(path, imports: Imports, useRequire) {
const declarations = [];
const inlines = [];
Expand Down Expand Up @@ -653,14 +672,21 @@ export default function (babel) {
for (const inlineFnName of inlines) {
inlineDeclarations.push(makeInlineStdlibFn(inlineFnName));
}
// insert inline fns before the first statement which isn't an import statement
for (const p of path.get("body")) {
if (!p.isImportDeclaration()) {
p.insertBefore(inlineDeclarations);
break;
}
}
insertAfterImports(path, inlineDeclarations);
}
}

function insertRuntimeHelpers(path) {
const helpers = [];
for (const helperName in path.scope.lscRuntimeHelpers) {
const fn = runtimeHelpers[helperName];
const uid = path.scope.lscRuntimeHelpers[helperName];
const fnAST = babel.template(fn.toString())({
[helperName]: uid,
});
helpers.push(fnAST);
}
insertAfterImports(path, helpers);
}

function generateForInIterator(path, type: "array" | "object") {
Expand Down Expand Up @@ -827,6 +853,103 @@ export default function (babel) {
}
}

function extendAndChain(andChainPath, condition) {
if (!andChainPath.node) {
andChainPath.replaceWith(condition);
} else {
andChainPath.replaceWith(t.logicalExpression("&&", andChainPath.node, condition));
}
}

function buildAnd(left, right) {
if (left && right) {
return t.logicalExpression("&&", left, right);
} else if (left) {
return left;
} else if (right) {
return right;
} else {
return t.booleanLiteral(true);
}
}

function buildTestForBinding(test, bindingPath, argRef) {
if (bindingPath.isObjectPattern()) {
const isObjUid = collectRuntimeHelper(bindingPath, "hasProps");

const propsToCheck = [];
const childPatterns = []; // list of [propName, path] tuples
for (const propPath of bindingPath.get("properties")) {
const propName = propPath.get("key").node.name;

if (propPath.get("value").isAssignmentPattern()) {
if (propPath.get("value.left").isPattern()) {
childPatterns.push([propName, propPath.get("value.left"), propPath.get("value.right").node]);
}
} else {
const propStr = t.stringLiteral(propName);
propsToCheck.push(propStr);

if (propPath.get("value").isPattern()) {
childPatterns.push([propName, propPath.get("value")]);
}
}
}

const isObjCall = t.callExpression(isObjUid, [argRef, t.arrayExpression(propsToCheck)]);
test = buildAnd(test, isObjCall);

for (const [ propName, childPatternPath, defaultObj = null ] of childPatterns) {
const propertyArgRef = t.memberExpression(argRef, t.identifier(propName));

if (defaultObj) {
test = buildAnd(test, t.logicalExpression("||",
buildTestForBinding(null, childPatternPath, propertyArgRef),
buildTestForBinding(null, childPatternPath, defaultObj)
));
} else {
test = buildTestForBinding(test, childPatternPath, propertyArgRef);
}
}
} else if (bindingPath.isArrayPattern()) {
const hasLengthUid = collectRuntimeHelper(bindingPath, "hasLength");

const childPatterns = []; // list of [index, path] tuples.
let minLength = 0;
let maxLength = 0;
bindingPath.get("elements").forEach((elemPath, i) => {
if (elemPath.isAssignmentPattern()) {
++maxLength;
if (elemPath.get("left").isPattern()) {
childPatterns.push([i, elemPath.get("left")]);
}
} else if (elemPath.isRestElement()) {
maxLength = null;
} else {
++minLength;
++maxLength;
if (elemPath.isPattern()) {
childPatterns.push([i, elemPath]);
}
}
});

const hasLengthCall = t.callExpression(hasLengthUid, [
argRef,
t.numericLiteral(minLength),
maxLength === null ? null : t.numericLiteral(maxLength)
].filter(x => x !== null));
test = buildAnd(test, hasLengthCall);

for (const [index, childPatternPath] of childPatterns) {
const elementArgRef = t.memberExpression(argRef, t.numericLiteral(index), true);
test = buildTestForBinding(test, childPatternPath, elementArgRef);
}
} else throw new TypeError(`Expected Pattern, got ${bindingPath.node.type}`);

return test;
}

function transformMatchCases(argRef, cases) {
return cases.reduce((rootIf, path) => {

Expand All @@ -843,6 +966,10 @@ export default function (babel) {
// add binding (and always use block bodies)
ensureBlockBody(path, "consequent");
if (path.node.binding) {
const bindingTest = buildTestForBinding(null, path.get("binding"), argRef);
const testWithBindingTest = buildAnd(path.get("test").node, bindingTest)
path.get("test").replaceWith(testWithBindingTest);

const bindingDecl = t.variableDeclaration("const", [
t.variableDeclarator(path.node.binding, argRef)
]);
Expand Down Expand Up @@ -1128,6 +1255,7 @@ export default function (babel) {
const stdlib: Stdlib = initializeStdlib(state.opts);
const useRequire = state.opts.stdlib && state.opts.stdlib.require === true;
const imports: Imports = {};
path.scope.lscRuntimeHelpers = {};

path.traverse({

Expand Down Expand Up @@ -1451,6 +1579,7 @@ export default function (babel) {
});

insertStdlibImports(path, imports, useRequire);
insertRuntimeHelpers(path);
}

return {
Expand Down
21 changes: 21 additions & 0 deletions src/stdlib.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ export const lightscriptImports = {
"bitwiseZeroFillRightShift": "inline",
};

export const runtimeHelpers = {
Copy link
Contributor

Choose a reason for hiding this comment

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

I liked this way of doing helpers a lot, so I tried to adopt it. When I applied it to my local build of lscdiag I got some very strange code output in the generated helpers. Took me a while to figure out why -- turns out that minifiers mangle these things and then fn.toString() brings in the minified code.

We could just say we don't care about that, but I think live compilers and minifiers are good things generally, whereas this, while neat, could just be replaced with a plain string passed to babel-template. I recommend the latter approach.

Copy link
Contributor Author

@rattrayalex rattrayalex Jun 9, 2017

Choose a reason for hiding this comment

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

ah, yeah, that's why. I kind of don't mind – it's babel helping make sure that I'm writing cross-browser code. But yes, if you see the tests, the output isn't the exact same as what I wrote.

Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't sound quite right... the transform being run here is just the one specified in .babelrc for the plugin, which is just env: { node: 4 } -- that's definitely not cross browser code.

I think having cross-browser code is a good point, but might be better to paste the helper into babel-repl, set the env to Netscape Navigator 1.0, and paste the output into a plain string.

Alternatively, an extra build step could compile the helpers with a different env? But that seems like overtooling.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My current implementation of hasProps and hasLength doesn't change due to this factor, which I think is desirable, so I think I'll leave this as-is for now, hope that's ok

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I hadn't read that comment very carefully – I did check in the babel repl that with an ie<6 target the output doesn't change

hasProps: function hasProps(obj, props) {
return (
obj != null &&
(typeof obj === "object" || typeof obj === "function") &&
props.filter(prop => prop in obj).length === props.length
);
},
hasLength: function hasLength(arr, minLength, maxLength) {
minLength = minLength || 0;
maxLength = maxLength != null ? maxLength : Number.MAX_SAFE_INTEGER;
return (
arr != null &&
typeof arr !== "function" &&
arr.length === arr.length|0 &&
arr.length >= minLength &&
arr.length <= maxLength
);
},
};

export const everyLodashMethod = [
"add",
"after",
Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/match/arr-pattern/actual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
match x:
| []:
"empty"
| [ a, b ]:
a - b
| [ a, b = 2 ]:
a + b - 2
| [ a, ...b ]:
b.concat(a)
| [
[
b
d = 'e'
]
[ g, , h ]
...j
]:
[b, d, g, ...j].join('')
92 changes: 92 additions & 0 deletions test/fixtures/match/arr-pattern/exec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
assert.equal(
"empty",
match []:
| []:
"empty"
)
assert.equal(
"empty",
match []:
| []:
"empty"
)
assert.equal(
undefined,
match []:
| [a]:
a + 1
)
assert.equal(
undefined,
match [1]:
| [a, b]:
a + b
)
assert.equal(
5,
match [1]:
| [a, b = 4]:
a + b
)
assert.equal(
3,
match [1, 2]:
| [a, b]:
a + b
)
assert.equal(
4,
match [1, 2, 3]:
| [a,, b]:
a + b
)
assert.equal(
undefined,
match [1, 2]:
| [a,, b]:
a + b
)
assert.deepEqual(
[2, 3, 1],
match [1, 2, 3]:
| [a, ...b]:
b.concat(a)
)
assert.deepEqual(
[1, 4],
match [4]:
| [a, b = 1, ...c]:
c.concat([b, a])
)
assert.deepEqual(
[6, 7, 5, 4],
match [4, 5, 6, 7]:
| [a, b = 1, ...c]:
c.concat([b, a])
)
assert.deepEqual(
[1, 2, 4, 6, 7, 8],
match [[1], [4, 5, 6], 7, 8]:
| [
[
b
d = 2
]
[ g, , h ]
...j
]:
[b, d, g, h, ...j]
)
assert.deepEqual(
undefined,
match [[1], [4, 5], 7, 8]:
| [
[
b
d = 2
]
[ g, , h ]
...j
]:
[b, d, g, h, ...j]
)
27 changes: 27 additions & 0 deletions test/fixtures/match/arr-pattern/expected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function _hasLength(arr, minLength, maxLength) {
minLength = minLength || 0;
maxLength = maxLength != null ? maxLength : Number.MAX_SAFE_INTEGER;
return arr != null && typeof arr !== "function" && arr.length === arr.length | 0 && arr.length >= minLength && arr.length <= maxLength;
}

if (_hasLength(x, 0, 0)) {
const [] = x;

"empty";
} else if (_hasLength(x, 2, 2)) {
const [a, b] = x;

a - b;
} else if (_hasLength(x, 1, 2)) {
const [a, b = 2] = x;

a + b - 2;
} else if (_hasLength(x, 1)) {
const [a, ...b] = x;

b.concat(a);
} else if (_hasLength(x, 2) && _hasLength(x[0], 1, 2) && _hasLength(x[1], 3, 3)) {
const [[b, d = 'e'], [g,, h], ...j] = x;

[b, d, g, ...j].join('');
}
14 changes: 14 additions & 0 deletions test/fixtures/match/obj-pattern/actual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
match x:
| { a }:
a
| { a, b }:
a + b
| { a, b = 1 }:
a + b
| { a, b: { ba, bb = 1 }, c: { ca, cb: { cba } } }:
a + ba + bb + ca + cba
| 1 or 2 with { a, b: { c } }:
a + c
| { a: { b: { c } } = otherObj }:
c
//TODO: | { a, ...b }: b
Loading