Skip to content

Commit

Permalink
finish README and tweak ordering for contrived example
Browse files Browse the repository at this point in the history
planttheidea committed Jun 6, 2019

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent e69acf6 commit a2673b4
Showing 6 changed files with 249 additions and 81 deletions.
83 changes: 73 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
# inline-loops.macro

Automatic inlining of loops for performance boost
Iteration helpers that inline to native loops for performance

## Table of Contents

- [inline-loops.macro](#inline-loopsmacro)
- [Table of Contents](#table-of-contents)
- [Summary](#summary)
- [Usage](#usage)
- [Methods](#methods)
- [How it works](#how-it-works)
- [Gotchas](#gotchas)
- [`*Object` methods do not perform `hasOwnProperty` check](#object-methods-do-not-perform-hasownproperty-check)
- [`findIndex` vs `findKey`](#findindex-vs-findkey)
- [Development](#development)

## Summary

`inline-loops.macro` is a babel macro that will inline calls to the iteration methods provided, replacing them with `for` loops (or `for-in` in the case of objects). While this adds more code, it is also considerably more performant than the native versions of these methods. When working in non-JIT environments this is also faster than equivalent runtime helpers, as it avoids function calls and inlines operations when possible.

This is inspired by the work done on [babel-plugin-loop-optimizer](https://www.npmjs.com/package/babel-plugin-loop-optimizer), but aims to be both more targeted and more full-featured. Rather than globally replace all native calls, the use of macros allow a controlled, opt-in usage. You can use it for everything, only for hotpaths, as a replacement for `lodash` with legacy support, whatever you see fit for your project.
This is inspired by the work done on [babel-plugin-loop-optimizer](https://www.npmjs.com/package/babel-plugin-loop-optimizer), but aims to be both more targeted and more full-featured. Rather than globally replace all native calls, the use of macros allow a controlled, opt-in usage. This macro also supports decrementing array and object iteration, as well as nested usage.

You can use it for everything, only for hotpaths, as a replacement for `lodash` with legacy support, whatever you see fit for your project. The support should be the same as the support for `babel-plugin-macros`.

## Usage

@@ -17,7 +30,10 @@ import { map, reduce, someObject } from 'inline-loops.macro';

function contrivedExample(array) {
const doubled = map(array, (value) => value * 2);
const doubleObject = reduce(doubled, (object, value) => ({ ...object, [value]: value });
const doubleObject = reduce(doubled, (object, value) => ({
...object,
[value]: value
});

if (someObject(doubleObject, (value) => value > 100)) {
console.log('I am large!');
@@ -90,13 +106,45 @@ for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
const doubled = _result;
```
Notice that there is no reference to the original function, because it used the return directly.
Notice that there is no reference to the original function, because it used the return directly. This even works with nested calls!
```javascript
// this
const isAllTuples = every(array, tuple =>
every(tuple, (value) => Array.isArray(value) && value.length === 2)
);

// becomes this
let _result = true;

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];

let _result2 = true;

for (let _key2 = 0, _length2 = _value.length, _value2; _key2 < _length2; ++_key2) {
_value2 = _value[_key2];

if (!(Array.isArray(_value2) && _value2.length === 2)) {
_result2 = false;
break;
}
}

if (!_result2) {
_result = false;
break;
}
}

const isAllTuples = _result;
```
## Gotchas
There are a few limitations for this macro to be aware of:
Some aspects of implementing this macro that you should be aware of:
1. `*Object` methods do not perform `hasOwnProperty` check
### `*Object` methods do not perform `hasOwnProperty` check
The object methods will do operations in `for-in` loop, but will not guard via a `hasOwnProperty` check. For example:
@@ -117,31 +165,31 @@ for (let _key in object) {
const doubled = _result;
```
The need for `hasOwnProperty` checks are often an edge case, as it only matters when using objects created via a custom constructor, iterating over static properties on functions, or other non-standard operations. This is a slowdown in all environments, but they can be especially expensive in legacy browsers or non-JIT environments.
This works in a vast majority of cases, as the need for `hasOwnProperty` checks are often an edge case; it only matters when using objects created via a custom constructor, iterating over static properties on functions, or other non-standard operations. `hasOwnProperty` is a slowdown, but can be especially expensive in legacy browsers or non-JIT environments.
If you need to incorporate this, you can do it one of two ways:
**Add filtering (iterates twice, but arguably cleaner semantics)**
```javascript
// this
const raw = mapObject(object, (value, key) => Object.prototype.hasOwnProperty.call(object, key) ? value * 2 : null);
const raw = mapObject(object, (value, key) => object.hasOwnProperty(key) ? value * 2 : null);
const doubled = filterObject(raw, value => value !== null);
```
**Use reduce instead (iterates only once, but a little harder to grok)**
```javascript
const doubled = reduceObject(object, (_doubled, value, key) => {
if (Object.prototype.hasOwnProperty.call(object, key)) {
if (object.hasOwnProperty(key)) {
_doubled[key] = value * 2;
}

return _doubled;
});
```
1. `findIndex` vs `findKey`
### `findIndex` vs `findKey`
Most of the operations follow the same naming conventions:
@@ -150,3 +198,18 @@ Most of the operations follow the same naming conventions:
- `{method}Object` (object)
The exception to this is `findIndex` / `findKey` (which are specific to arrays) and `findKey` (which is specific to objects). The rationale should be obvious (arrays only have indices, objects only have keys), but because it is the only exception to the rule I wanted to call it out.
## Development
Standard stuff, clone the repo and `npm install` dependencies. The npm scripts available:
- `build` => runs babel to transform the macro for legacy NodeJS support
- `copy:types` => copies `index.d.ts` to `build`
- `dist` => runs `build` and `copy:types`
- `lint` => runs ESLint against all files in the `src` folder
- `lint:fix` => runs `lint``, fixing any errors if possible
- `prepublishOnly` => run `lint`, `test`, `test:coverage`, and `dist`
- `release` => release new version (expects globally-installed `release-it`)
- `release:beta` => release new beta version (expects globally-installed `release-it`)
- `test` => run jest tests
- `test:watch` => run `test`, but with persistent watcher
13 changes: 13 additions & 0 deletions __tests__/__fixtures__/nested/contrived/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { map, reduce, someObject } from '../../../../inline-loops.macro';

if (
someObject(
reduce(map(array, value => value * 2), (object, value) => ({
...object,
[value]: value,
})),
value => value > 100,
)
) {
console.log('I am large!');
}
35 changes: 35 additions & 0 deletions __tests__/__fixtures__/nested/contrived/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];

_result.push(_value * 2);
}

const _fn2 = (object, value) => ({ ...object,
[value]: value
});

let _result2 = _result[0];

for (let _key2 = 1, _length2 = _result.length, _value2; _key2 < _length2; ++_key2) {
_value2 = _result[_key2];
_result2 = _fn2(_result2, _value2, _key2, _result);
}

let _result3 = false;

let _value3;

for (let _key3 in _result2) {
_value3 = _result2[_key3];

if (_value3 > 100) {
_result3 = true;
break;
}
}

if (_result3) {
console.log('I am large!');
}
144 changes: 95 additions & 49 deletions build/inline-loops.macro.js
Original file line number Diff line number Diff line change
@@ -713,12 +713,33 @@ function getCallTypes(references, method) {
var decrementingCalls = references[decrementingMethod] || [];
var objectCalls = references[objectMethod] || [];
return {
decrementingCalls: decrementingCalls,
decrementingCalls: decrementingCalls.map(function (path) {
return {
method: decrementingMethod,
path: path,
sourceMethod: method,
type: 'decrementing'
};
}),
decrementingMethod: decrementingMethod,
incrementingCalls: incrementingCalls,
incrementingCalls: incrementingCalls.map(function (path) {
return {
method: method,
path: path,
sourceMethod: method,
type: 'incrementing'
};
}),
isArrayOnly: isArrayOnly,
isObjectOnly: isObjectOnly,
objectCalls: objectCalls,
objectCalls: objectCalls.map(function (path) {
return {
method: objectMethod,
path: path,
sourceMethod: method,
type: 'object'
};
}),
objectMethod: objectMethod
};
}
@@ -746,6 +767,51 @@ function inlineLoops(_ref12) {

return allMethods.push.apply(allMethods, _toConsumableArray(incrementingCalls).concat(_toConsumableArray(decrementingCalls), _toConsumableArray(objectCalls)));
});
allMethods.forEach(function (_ref13) {
var path = _ref13.path;
path.node.__inlineLoopsMacro = true;
});
allMethods.sort(function (_ref14, _ref15) {
var a = _ref14.path;
var b = _ref15.path;
var aContainer = a.container;
var bContainer = b.container;

if (aContainer.arguments) {
var _aContainer$arguments = _slicedToArray(aContainer.arguments, 1),
iterable = _aContainer$arguments[0];

if (t.isCallExpression(iterable) && iterable.callee.__inlineLoopsMacro) {
return 1;
}
}

if (bContainer.arguments) {
var _bContainer$arguments = _slicedToArray(bContainer.arguments, 1),
_iterable = _bContainer$arguments[0];

if (t.isCallExpression(_iterable) && _iterable.callee.__inlineLoopsMacro) {
return -1;
}
}

var aStart = a.node.loc.start;
var bStart = b.node.loc.start;

if (bStart.line > aStart.line) {
return -1;
}

if (aStart.line > bStart.line) {
return 1;
}

if (bStart.column > aStart.column) {
return -1;
}

return 1;
});
var handlers = {
every: handleEvery,
filter: handleFilter,
@@ -758,47 +824,36 @@ function inlineLoops(_ref12) {
some: handleSome
};

function createHandler(name, transform, isDecrementing, isObject) {
function createTransformer(name, transform, isDecrementing, isObject) {
return function _transform(path) {
if (path.findParent(function (_path) {
return _path.isConditionalExpression();
})) {
throw new MacroError("You cannot use ".concat(name, " in a conditional expression."));
}

var callee = path.parentPath.parent.callee;
var args = path.parent.arguments;

if (callee) {
var ancestorPath = path.parentPath;
if (args.some(function (arg) {
return t.isSpreadElement(arg);
})) {
throw new MacroError('You cannot use spread arguments with the macro, please declare the arguments explicitly.');
}

while (ancestorPath) {
if (ancestorPath.node && ancestorPath.node.body) {
break;
}
var _args = _slicedToArray(args, 3),
object = _args[0],
handler = _args[1],
initialValue = _args[2];

if (t.isCallExpression(ancestorPath)) {
(function () {
var expression = ancestorPath.parent.expression;
var caller = expression ? expression.callee : ancestorPath.parent.callee;

if (allMethods.find(function (_ref13) {
var node = _ref13.node;
return node === caller && node !== path.node;
})) {
throw new MacroError("You cannot nest looper methods. You should store the results of ".concat(name, " to a variable, and then call ").concat(path.parentPath.parent.callee.name, " with it."));
}
})();
}
var isHandlerMacro = allMethods.find(function (_ref16) {
var methodPath = _ref16.path;
return methodPath.node !== path.node && handler === methodPath.node;
});

ancestorPath = ancestorPath.parentPath;
}
if (isHandlerMacro) {
throw new MacroError('You cannot use the macro directly as a handler, please wrap it in a function call.');
}

var _path$parent$argument = _slicedToArray(path.parent.arguments, 3),
object = _path$parent$argument[0],
handler = _path$parent$argument[1],
initialValue = _path$parent$argument[2];

transform({
t: t,
path: path,
@@ -811,24 +866,15 @@ function inlineLoops(_ref12) {
};
}

METHODS.forEach(function (method) {
var _getCallTypes2 = getCallTypes(references, method),
decrementingCalls = _getCallTypes2.decrementingCalls,
decrementingMethod = _getCallTypes2.decrementingMethod,
incrementingCalls = _getCallTypes2.incrementingCalls,
isArrayOnly = _getCallTypes2.isArrayOnly,
isObjectOnly = _getCallTypes2.isObjectOnly,
objectCalls = _getCallTypes2.objectCalls,
objectMethod = _getCallTypes2.objectMethod;

if (!isObjectOnly) {
incrementingCalls.forEach(createHandler(method, handlers[method]));
decrementingCalls.forEach(createHandler(decrementingMethod, handlers[method], true));
}

if (!isArrayOnly) {
objectCalls.forEach(createHandler(objectMethod, handlers[method], false, true));
}
allMethods.forEach(function (_ref17) {
var method = _ref17.method,
path = _ref17.path,
sourceMethod = _ref17.sourceMethod,
type = _ref17.type;
var isDecrementing = type === 'decrementing';
var isObject = type === 'object';
var handler = createTransformer(method, handlers[sourceMethod], isDecrementing, isObject);
handler(path);
});
}

Loading

0 comments on commit a2673b4

Please sign in to comment.