Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
98f407e
Copy wrappers
jfmengels Dec 14, 2021
475fa76
Add start
jfmengels Dec 14, 2021
df2bc38
Add extractMapCall
jfmengels Dec 14, 2021
76b1c91
Replace consecutive List.map calls
jfmengels Dec 14, 2021
dd4e7fb
Rename expressions
jfmengels Dec 14, 2021
de9dfbd
Reframe test title
jfmengels Dec 14, 2021
22f96b0
Add test
jfmengels Dec 14, 2021
c2e6ad0
Rename
jfmengels Dec 14, 2021
2bbb82a
Destructure
jfmengels Dec 14, 2021
83f330e
Fuse List.filter
jfmengels Dec 14, 2021
da8588c
Rename tests
jfmengels Dec 14, 2021
1a07a3a
Don't fuse different operations together
jfmengels Dec 14, 2021
3bfd8de
Merge conditions
jfmengels Dec 14, 2021
9018ec1
Remove constant
jfmengels Dec 14, 2021
33a05ea
Explain what the code corresponds to
jfmengels Dec 14, 2021
3caed3b
Add failing test
jfmengels Dec 14, 2021
47ad5f3
Add failing test
jfmengels Dec 14, 2021
b07356e
Extract function
jfmengels Dec 14, 2021
3cbe339
Add extractComposition
jfmengels Dec 14, 2021
658b1cc
Support composing functions
jfmengels Dec 14, 2021
aa1e97d
Make composition function dependent on the operation
jfmengels Dec 15, 2021
358ea38
Support filterMap composition
jfmengels Dec 15, 2021
6bb99db
Support Set.map composition
jfmengels Dec 15, 2021
54d5f71
Support Array.map composition
jfmengels Dec 15, 2021
171fbd6
Support Array.filter composition
jfmengels Dec 15, 2021
ccc208e
Support Set.filter composition
jfmengels Dec 15, 2021
5d4524f
Document potential improvements
jfmengels Dec 15, 2021
1bab2f5
Re-organize tests
jfmengels Dec 15, 2021
9e5ea02
Support fusing Html.map
jfmengels Dec 18, 2021
d1ce8b7
Add test for chained map functions
jfmengels Dec 15, 2021
0ff859e
Add notes on making it back into the compiler
jfmengels Dec 19, 2021
82dd666
Stop supporting filter fusion
jfmengels Dec 19, 2021
67100ed
Use List.filterMap for the test about mixing operations
jfmengels Dec 19, 2021
2925c5d
Fix test documentation
jfmengels Dec 19, 2021
a85b0b1
Add tests for composing List.map
jfmengels Dec 16, 2021
5c2ca0b
Add test for composing List.filterMap
jfmengels Dec 19, 2021
2b4b980
Mention stream-fusion
jfmengels Dec 19, 2021
b9a7531
Support composing elm/parser map function
jfmengels Dec 26, 2021
20dd519
Support composing elm/json Json.Decode.map
jfmengels Dec 26, 2021
a12e4c0
Support composing Maybe.map
jfmengels Dec 26, 2021
7cf4757
Support composing Cmd.map
jfmengels Dec 26, 2021
e9d5b47
Support composing Sub.map
jfmengels Dec 26, 2021
bfcea77
Support composing Result.map
jfmengels Dec 26, 2021
bc134dc
Support composing String.map
jfmengels Dec 26, 2021
13efa51
Formatting
jfmengels Dec 26, 2021
9b1ccf2
Support composing Task.map
jfmengels Dec 26, 2021
3effa74
Support composing Bytes.Decode.map
jfmengels Dec 26, 2021
33a4f0d
Support composing Html.Attributes.map
jfmengels Dec 26, 2021
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
240 changes: 240 additions & 0 deletions src/transforms/operationsFusion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import ts from 'typescript';

/* Combine operations into one, such as successive List.map calls.

Indirectly inspired by https://hackage.haskell.org/package/stream-fusion-0.1.2.5/docs/Data-List-Stream.html
which can be a source of inspiration for more fusion transforms.

```js
// Elm
x |> List.map f1 |> List.map f2
// JS code
A2($elm$core$List$map, f2, A2($elm$core$List$map, f1, x))
```
This transforms changes the JS code to
```elm
// JS code
A2($elm$core$List$map, A2($elm$core$Basics$composeR, f1, f2), x)
// so that it becomes equivalent to the following Elm code
x |> List.map (f1 >> f2)
```


It supports compositions of composed functions:

```js
// Elm: List.map f1 >> List.map f2
// Before
A2($elm$core$Basics$composeR, $elm$core$List$map(f1), $elm$core$List$map(f2))
// After
$elm$core$List$map(A2($elm$core$Basics$composeR, f1, f2))
```

*/

// Potential improvements:
// - Should we support moving List.take and List.drop functions to before List.map (not before List.filter)?
// - Should we support combining `x |> List.take a |> List.take b` into `x |> List.take (Math.min a b)`?
// - Should we support combining `x |> List.drop a |> List.drop b` into `x |> List.drop (a + b)`?
// - Should we support combining `x |> Dict.map f |> Dict.map g` into `x |> Dict.map (\index a -> g index (f index a))`?



/* Notes on supporting this for custom functions by user and libraries.

For `elm-optimize-level-2`, users could specify a configuration for the tool where
they'd list which functions could support their optimization.

For the compiler, functions could include keywords in their documentation to indicate
what optimizations to apply.

{-| Maps from a to b.

@optimation fusion(map)

-- Or alternatively
@optimation fusion(filter)
@optimation fusion(filterMap)

-}
map : (a -> b) -> X a -> X b

and then the compiler could also apply this optimization for all of the chains that contain this function call.

The bigger question is how to make sure that the compiler doesn't blindly trust that this optimization can be applied.

map : (a -> b) -> X a -> X b
map fn (X a) =
X { a
| apply = a.apply >> fn
, mapCount = a.mapCount + 1
}

For instance fusing 2 applications of the function above would lead to different results than having them
applied multiple times (because `mapCount` would be incremented once instead of twice).

I don't know if there is a way for the compiler to prove or find out that `map f >> map g` is the same
as `map (f >> g)`. Maybe the compiler could try doing this and report an error when it could not
determine this to be true (or when it actively knows it's different).

*/

const COMPOSE_LEFT = "$elm$core$Basics$composeL";
const COMPOSE_RIGHT = "$elm$core$Basics$composeR";

const supportedFusions : Record<string, CompositionFn> = {
"$elm$core$List$map": composeFunctions,
"$elm$core$String$map": composeFunctions,
"$elm$core$List$filterMap": filterMapComposition,
"$elm$core$Set$map": composeFunctions,
"$elm$core$Array$map": composeFunctions,
"$elm$core$Maybe$map": composeFunctions,
"$elm$core$Result$map": composeFunctions,
"$elm$core$Task$map": composeFunctions,
"$elm$core$Platform$Cmd$map": composeFunctions,
"$elm$core$Platform$Sub$map": composeFunctions,
"$elm$html$Html$map": composeFunctions,
"$elm$html$Html$Attributes$map": composeFunctions,
"$elm$json$Json$Decode$map": composeFunctions,
"$elm$bytes$Bytes$Decode$map": composeFunctions,
"$elm$parser$Parser$map": composeFunctions,
"$elm$parser$Parser$Advanced$map": composeFunctions,
};

export const operationsFusion : ts.TransformerFactory<ts.SourceFile> = (context: any) => {
return (sourceFile) => {
const visitor = (originalNode: ts.Node): ts.VisitResult<ts.Node> => {
const node = ts.visitEachChild(originalNode, visitor, context);

if (!ts.isCallExpression(node)) { return node; }

return fuse(node)
|| extractComposition(node)
|| node;
};

return ts.visitNode(sourceFile, visitor);
};
};

function fuse(node: ts.CallExpression) : ts.CallExpression | null {
const outerCallExtract = extractCall(node);
if (!outerCallExtract) { return null; }

const innerCallExtract = extractCall(outerCallExtract.dataArg);
if (!innerCallExtract
|| outerCallExtract.operation.text !== innerCallExtract.operation.text
) {
return null;
}

return ts.createCall(
ts.createIdentifier("A2"),
undefined,
[
innerCallExtract.operation,
outerCallExtract.compositionFn(innerCallExtract.fnArg, outerCallExtract.fnArg),
innerCallExtract.dataArg
]
);
}

function extractCall(node: ts.Expression) : { compositionFn : CompositionFn, operation: ts.Identifier, fnArg : ts.Expression, dataArg : ts.Expression } | null {
if (ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& node.expression.text === "A2"
) {
const [operation, fnArg, dataArg] = node.arguments;
if (ts.isIdentifier(operation)
&& supportedFusions.hasOwnProperty(operation.text)
) {
return {
compositionFn: supportedFusions[operation.text],
operation,
fnArg,
dataArg
};
}
}

return null;
}

function extractComposition(node: ts.CallExpression) : ts.CallExpression | null {
if (ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& node.expression.text === "A2"
) {
const [fn, firstArg, secondArg] = node.arguments;
if (!ts.isIdentifier(fn)
|| !(fn.text === COMPOSE_LEFT || fn.text === COMPOSE_RIGHT)
) {
return node;
}

const [fn1, fn2] =
fn.text === COMPOSE_RIGHT
? [firstArg, secondArg]
: [secondArg, firstArg];

const firstArgExtract = extractCompositionCall(fn1);
if (!firstArgExtract) { return null; }

const secondArgExtract = extractCompositionCall(fn2);
if (!secondArgExtract || firstArgExtract.operation !== secondArgExtract.operation) {
return null;
}

return ts.createCall(
ts.createIdentifier(secondArgExtract.operation),
undefined,
[ secondArgExtract.compositionFn(firstArgExtract.arg, secondArgExtract.arg) ]
);
}
return null;
}

function extractCompositionCall(node: ts.Expression) : { compositionFn : CompositionFn, operation: string, arg : ts.Expression } | null {
if (ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& supportedFusions.hasOwnProperty(node.expression.text)
) {
return {
compositionFn: supportedFusions[node.expression.text],
operation: node.expression.text,
arg: node.arguments[0]
};
}

return null;
}

type CompositionFn = (x: ts.Expression, y: ts.Expression) => ts.CallExpression;

function composeFunctions(functionToApplyFirst : ts.Expression, functionToApplySecond : ts.Expression) : ts.CallExpression {
return ts.createCall(
ts.createIdentifier("A2"),
undefined,
[
ts.createIdentifier(COMPOSE_RIGHT),
functionToApplyFirst,
functionToApplySecond
]
);
}

function filterMapComposition(functionToApplyFirst : ts.Expression, functionToApplySecond : ts.Expression) : ts.CallExpression {
return ts.createCall(
ts.createIdentifier("A2"),
undefined,
[
ts.createIdentifier(COMPOSE_RIGHT),
functionToApplyFirst,
ts.createCall(
ts.createIdentifier("$elm$core$Maybe$andThen"),
undefined,
[functionToApplySecond]
)
]
);
}
3 changes: 3 additions & 0 deletions src/transforms/utils/wrappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const invocationRegex = /^A(?<arity>[1-9]+[0-9]*)$/;

export const wrapperRegex = /^F(?<arity>[1-9]+[0-9]*)$/;
Loading