Because IIFEs aren’t adequate for either smart pipelines, we need to create helper functions that allow us to “flatten” statements that contain them, extracting them into preceding constant declarations. This would not only allow us to implement smart pipelines properly, this would also allow us to implement do
expressions away from using IIFEs, which do not work with await
and yield
(see babel/babel#3780).
-
Add a
getChildOfInnermostStatement
method to paths, so that calling the method on a path tox
in…function f () { return Promise.all(g(x)); }
…would return a path pointing to the
Promise.all(g(x))
expression. The same thing would happen if this method is called on paths tog(x)
orPromise.all(g(x))
itself. (Calling it on a path to statements such as thereturn
statement will returnundefined
.)Similarly, calling this method on a path to
x
in…if (f(x)) g(y); else h(z);
…would return a path pointing to the
f(x)
expression. The same thing would happen if this method is called on a path tof(x)
itself. Calling the method on paths toy
or tog(y)
would return a path tog(y)
. The same would go for paths toz
or toh(z)
, returning a path toh(z)
. -
Add a
replaceTopicReferences
function that, given any main expression and a variable, replaces all instances ofPipelinePrimaryTopicReference
in the main expression (except references contained within an inner pipeline’s body) with that variable. For instance, using this function on# + (# |> # * 2) + 1
with an$0
variable will return$0 + ($0 |> # * 2) + 1
. -
Create an
isPureExpression
function that, given any type of expression or statement, returns a boolean for whether the expression is pure or impure (i.e., whether its evaluation may cause any observable side effects, including runtime errors). The order in which a program’s impure expressions are evaluated must be preserved by any code transformation, but the order of pure expressions’ evaluation may be rearranged without affecting the program’s behavior. The function’s behavior is such that:- It would return
true
for any literal boolean/number/string, liketrue
,false
,1
,"string"
. - It would return
true
for any variable reference, likex
. - It would return
true
for any array literal whose elements are all pure expressions, like[0, 1]
,[[0, 1], 2]
,[f(0, 1), 2]
. - It would return
true
for any object literal whose keys and values are all pure expressions, like{x: 0, y: 1}
,{a: {x: 0, y: 1}, b: 2}
,{x: f(0, 1), y: 2}
. - It would return
true
for any function-definition expression. - It would return
true
for any comma expression or logical-not/and/or/conditional expression whose child subexpression(s) are themselves pure. - It would return
false
for any other expression, including pipeline expressions, function/constructor calls, assignment expressions, arithmetic/bitwise expressions (which can raise runtime errors), member access,await
expressions,yield
expressions, anddo
expressions.
- It would return
-
Create a
flattenExpressionOnce
function that, given any type of expression, returns a shallowly flattened version of that expression – along with a series of helper declarations for lexically hygienic new helper constants or helper variables. If the expression cannot be flattened (if it is already flat), then it returnsnull
instead.Category Original Flattened Helper declarations A 0
Already flat None B await x
Already flat None C await f()
await $0
const $0 = f();
C g(f(), 0)
g($0, 0)
const $0 = f();
C [f(), 0]
[$0, 0]
const $0 = f();
C {[a]: b, [f()]: g()}
{[a]: b, [$0]: $1}
const $0 = f(); const $1 = g();
C {[x |> # + 1]: y}
{[$0]: y}
const $0 = x |> # + 1;
D x |> f
f(x)
None E x |> f |> g
g($0)
const $0 = x |> f;
E x |> # + 1 |> g
g($0)
const $0 = x |> # + 1;
F x |> # + 1
x + 1
None F x |> [f(), # + 1, # + 2]
[f(), x + 1, x + 2]
None F x |> [f(), # |> # + 1]
[f(), x |> # + 1]
None G g(x) |> [f(), # + 1, # + 2]
[f(), $0 + 1, $0 + 2]
const $0 = g(x);
G g(x) |> [f(), # |> # + 1]
[f(), $0 |> # + 1]
const $0 = g();
G x |> [f(), # + 1] |> [g(), h(#), i(#)]
[g(), h($0), i($0)]
const $0 = x |> [f(), # + 1];
H do { if (f()) g(); else h(); }
$0;
var $0; if (f()) $0 = g(); else $0 = h();
The examples above follow these rules:
-
A. When the expression contains no subexpression, then it is already flat and the function returns
null
. -
B. When the expression contains child subexpressions and none of those child subexpressions is impure, then it is already flat and the function returns
null
. -
C. When the expression contains child subexpressions and any one of those child subexpressions is impure, then its flattened version is the expression but with all of those impure child subexpressions replaced by lexically hygienic new helper constants.
-
D. When the expression is a bare-style pipeline whose head is a pure expression, then its flattened version is simply a function call on the pipeline’s body with the pipeline’s head. In this case, there are no helper declarations.
-
E. When the expression is a bare-style pipeline whose head is an impure expression, then its flattened version is a function call on a lexically hygienic new helper constant. The helper constant is defined as the the pipeline’s head expression.
-
F. When the expression is a topic-style pipeline whose head is a pure expression, then its flattened version is the result of calling
replaceTopicReferences
on the pipeline’s body expression with the pipeline’s head. In this case, there are no new helper declarations. -
G. When the expression is a topic-style pipeline whose head is an impure expression, then its flattened version is the result of first calling
replaceTopicReferences
on the pipeline’s body expression with a lexically hygienic new helper constant. The new helper constant is defined as the pipeline’s head expression. -
H. When the expression is a
do
expression, then its flattened version is a lexically hygienic new helper variable. The new helper variable’s declaration is followed by the child statements of the originaldo
expression’s block, except that the final statement of every branch from the block is assigned to the helper variable. The logic for adding such assignments to a block of statements is already implemented inreplaceExpressionWithStatements
, but, unlike withreplaceExpressionWithStatements
, the resulting statements would not be wrapped in an arrow function.
-
-
Create a
flattenExpression
function that, given any type of expression, returns a deeply flattened version of that expression – along with a series of helper declarations for lexically hygienic new helper constants or helper variables. If the expression cannot be flattened (if it is already flat), then it returnsnull
instead.This occurs by iterating first over the expression until it is flat (that is, until
flattenExpressionOnce
returnsnull
). Each time this occurs, any new helper declarations are appended to a growing list. Then,flattenExpression
is recursively applied to each helper declaration's binding expression. If it does not returnnull
, then a new helper declaration using the newly flattened binding expression replaces the old helper declaration in the list. Any additional helper declarations that this recursiveflattenExpression
call created are also inserted into the list, immediately before the current helper declaration. -
Create a
flattenAndReplaceChildOfStatement
function that requires a path to any expression whose parent is a statement (seegetChildOfInnermostStatement
above); otherwise it will throw an error. The function then replaces that expression with the result offlattenExpression
. It then inserts any new constant declarations in the appropriate location (usually, but not always, immediately before the replaced expression’s parent statement).For instance, the array expression in
return [f(), x, y |> # + 1 |> g(#, 2)];
is a child of a statement (thereturn
statement). And callingflattenExpression
on the expression would result in[$0, $1]
and the new declarationsconst $0 = f(); const $1 = y |> # + 1 |> g(#, 2);
. So then callingflattenAndReplaceChildOfStatement
on that array expression will effectively replace itsreturn
statement withconst $0 = f(); const $1 = y |> # + 1 |> g(#, 2); return [$0, x, $1];
.Now consider the pipeline expression
x |> # + 1 |> g(#, 2)
in that new code. It is now also a child of a statement (aconst
statement). CallingflattenStatement
on that pipeline expression would result inconst $1 = g($2, 1);
and the new declarationconst $2 = x |> # + 1;
. So then callingflattenAndReplaceStatement
on the pipeline expression will replace itsconst
statement withconst $2 = x |> # + 1; const $1 = g($2, 1);
.-
In general, when calling
flattenAndReplaceChildOfStatement
on any expression that is a child of a statement, then firstflattenExpression
is called on that expression. (flattenStatement
returns a new version of that statement along with any new declarations that it uses.) The original statement is replaced inline with the new version returned byflattenStatement
. Then the new declarations that it also returned will be inserted immediately before the statement. Several exceptions to this rule follow. -
If the parent expression is directly the antecedent or consequent child of an
if
statement – or if the expression is directly the body child of an arrow function or of afor
,for
–in
,for
–of
,while
,do
–while
,with
, or label statement – then the replacement statement must first be wrapped in a block statement. The new declarations will then be inserted within the block before the statement. For instance, flattening the pipeline expression in the antecedent statement of:if (f()) g() |> h(#, #+1) |> i; else j();
…would result in this:
if (f()) { const $0 = g(); const $1 = h($0, $0+1); i($1); } else j();
-
If the original expression was the
test
child of awhile (<test>) <body>
statement or ado <body> while (<test>) statement, then first
flattenExpressionis called on the
testexpression. This results in a replacement
testexpression, and a list of new declarations for the replacement
test. A declaration for an additional lexically hygienic new variable (called
$testbelow, which is initially bound to the replacement
test` expression) is also created. These parts are then combined such that the final statement looks like this:<testHelperDeclarations> var $test = <replacementTestExpression>; while ($test) { <body> <testHelperDeclarations> $test = <replacementTestExpression>; }
For instance, flattening the test expression in:
while (i |> f(#, #+1) |> g |> # < n) console.log(i);
…would result in this:
const $0 = f(i, i+1); const $1 = g($0); var $test = $1 < n; while ($test) { console.log(i); const $0 = f(i, i+1); const $1 = g($0); $test = $1 < n; }
-
If the original expression was the
test
child of a classicfor (<init>; <test>; <update>) <body>
statement, then firstflattenExpression
is called on thetest
expression. This results in a replacementtest
expression, and a list of new declarations for the replacementtest
. A declaration for an additional lexically hygienic new variable (called$test
below, which is initially bound to the replacementtest
expression) is also created. These parts are then combined such that the finalfor
statement looks like this:{ <initClause> <testHelperDeclarations> var $test = <replacementTestExpression>; for (; $test; <update>) { <body> <testHelperDeclarations> $test = <replacementTestExpression>; } }
For instance, flattening the test expression in:
for (var i = 0; i |> f(#, #+1) |> g |> # < n; i++) console.log(i);
…would result in this:
{ var i = 0; const $0 = f(i, i+1); const $1 = g($0); var $test = $1 < n; for (; $test; i++) { console.log(i); const $0 = f(i, i+1); const $1 = g($0); $test = $1 < n; } }
-
Once flattenAndReplaceExpression
is implemented, a visitor searching for pipeline expressions will be able to call flattenAndReplaceExpression
on the child of the innermost statement containing that pipeline expression (as determined by the getChildOfInnermostStatement
path method mentioned way above).
The visitor will then jump back to the path returned by flattenAndReplaceStatement
, which is the earliest statement that was just-now modified (and which will either be a new const
declaration or the innermost statement containing the pipeline itself). There, the visitor will restart its search for more pipeline expressions.
The visitor should perform this transformation inside-out: that is, upon leaving pipeline expressions, rather than when entering them.
do
expressions could similarly be implemented using this same code, rather than using IIFEs.