Skip to content

Commit

Permalink
ensure top-level conditionals are respected
Browse files Browse the repository at this point in the history
  • Loading branch information
planttheidea committed Dec 17, 2023
1 parent d10294c commit 0113cf5
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 18 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## Enhancements

- Support for conditional usage in ternaries / logical expressions
- Support for conditional and lazy scenarios (ternaries, logical expressions, default parameter assignments, etc.)
- Better TS typing
- Better inline handling of complex logic

Expand Down
60 changes: 50 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Iteration helpers that inline to native loops for performance
- [Methods](#methods)
- [How it works](#how-it-works)
- [Aggressive inlining](#aggressive-inlining)
- [Conditional / lazy scenarios](#conditional--lazy-scenarios)
- [Bailout scenarios](#bailout-scenarios)
- [Gotchas](#gotchas)
- [`*Object` methods do not perform `hasOwnProperty` check](#object-methods-do-not-perform-hasownproperty-check)
Expand Down Expand Up @@ -78,10 +79,8 @@ function contrivedExample(array) {
Internally Babel will transform these calls to their respective loop-driven alternatives. Example
```js
// this
const foo = map(array, fn);

// becomes this
// transforms to
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _value; _key < _length; ++_key) {
Expand All @@ -98,10 +97,8 @@ If you are passing uncached values as the array or the handler, it will store th
One extra performance boost is that `inline-loops` will try to inline the callback operations when possible. For example:
```js
// this
const doubled = map(array, (value) => value * 2);

// becomes this
// transforms to
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _value; _key < _length; ++_key) {
Expand All @@ -114,12 +111,10 @@ const doubled = _results;
Notice that there is no reference to the original function, because it used the return directly. This even works with nested calls!
```js
// this
const isAllTuples = every(array, (tuple) =>
every(tuple, (value) => Array.isArray(value) && value.length === 2),
);

// becomes this
// transforms to
let _determination = true;
for (
let _key = 0, _length = array.length, _tuple, _result;
Expand Down Expand Up @@ -149,6 +144,51 @@ for (
const isAllTuples = _determination;
```
### Conditional / lazy scenarios
There are times where you want to perform the operation lazily, and there is support for this as well:
```js
foo === 'bar' ? array : map(array, (v) => v * 2);
// transforms to
foo === 'bar'
? array
: (() => {
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _v; _key < _length; ++_key) {
_v = array[_key];
_results[_key] = _v * 2;
}
return _results;
})();
```
The wrapping in the IIFE (Immediately-Invoked Function Expression) allows for the lazy execution based on the condition, but if that condition is met then it eagerly executes and returns the value. This will work just as easily for default parameters:
```js
function getStuff(array, doubled = map(array, (v) => v * 2)) {
return doubled;
}
// transforms to
function getStuff(
array,
doubled = (() => {
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _v; _key < _length; ++_key) {
_v = array[_key];
_results[_key] = _v * 2;
}
return _results;
})(),
) {
return doubled;
}
```
Because there is a small cost to parse, analyze, and execute the function compared to just have the logic in the same closure, the macro will only wrap the logic in an IIFE if such conditional or lazy execution is required.
### Bailout scenarios
Inevitably not everything can be inlined, so there are known bailout scenarios:
Expand Down Expand Up @@ -188,7 +228,7 @@ The object methods will do operations in `for-in` loop, but will not guard via a
// this
const doubled = mapObject(object, (value) => value * 2);

// becomes this
// transforms to this
let _result = {};

let _value;
Expand Down
29 changes: 22 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,22 @@ export function replaceOrRemove(
}
}

export function isPossiblyDynamic(path: Path) {
if (path.isPattern()) {
return true;
}

if (
path.isBinaryExpression() ||
path.isCallExpression() ||
path.isTemplateLiteral()
) {
return isPossiblyDynamic(path.parentPath);
}

return path.isExpression();
}

export function shouldWrapInClosure(
path: Path<CallExpression>,
local: LocalReferences,
Expand All @@ -209,12 +225,13 @@ export function shouldWrapInClosure(
}

const functionParent = path.getFunctionParent();
const grandparentPath = parentPath.parentPath;

if (!functionParent || !grandparentPath) {
return false;
if (!functionParent) {
return isPossiblyDynamic(parentPath);
}

const grandparentPath = parentPath.parentPath;

if (parentPath.isPattern() || local.contents.length > 1) {
return true;
}
Expand All @@ -225,13 +242,11 @@ export function shouldWrapInClosure(
return contents.flat().some((content) => content.isVariableDeclaration());
}

if (!parentPath.isExpression()) {
if (!grandparentPath || !parentPath.isExpression()) {
return false;
}

const maybeNestedConditional =
grandparentPath.isPattern() ||
(grandparentPath.isExpression() && !grandparentPath.isBinaryExpression());
const maybeNestedConditional = isPossiblyDynamic(grandparentPath);

if (parentPath.isLogicalExpression()) {
return (
Expand Down

0 comments on commit 0113cf5

Please sign in to comment.