Skip to content

Add supportArraysForHtml transformer #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
37 changes: 37 additions & 0 deletions src/replacements/virtual-dom/_VirtualDom_nodeNS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
var _VirtualDom_nodeNS_usingArray = (namespace, tag, factList, kids) => {
for (var i = 0, descendantsCount = kids.length; i < kids.length; i++) {
descendantsCount += kids[i].b || 0;
}

return {
$: 1,
c: tag,
d: _VirtualDom_organizeFacts(factList),
e: kids,
f: namespace,
b: descendantsCount,
};
}, _VirtualDom_nodeNS = F2(function (namespace, tag) {
return F2(function (factList, kidList) {
if (Array.isArray(kidList)) {
return _VirtualDom_nodeNS_usingArray(namespace, tag, factList, kidList);
}

for (var kids = [], descendantsCount = 0; kidList.b; kidList = kidList.b) // WHILE_CONS
{
var kid = kidList.a;
descendantsCount += kid.b || 0;
kids.push(kid);
}
descendantsCount += kids.length;

return {
$: 1,
c: tag,
d: _VirtualDom_organizeFacts(factList),
e: kids,
f: namespace,
b: descendantsCount,
};
});
});
54 changes: 54 additions & 0 deletions src/replacements/virtual-dom/_VirtualDom_organizeFacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
function _VirtualDom_organizeFacts_usingArray(factArray) {
for (var i = 0, facts = {}; i < factArray.length; i++) {
var entry = factArray[i];

var tag = entry.$;
var key = entry.n;
var value = entry.o;

if (tag === "a2") {
key === "className"
? _VirtualDom_addClass(facts, key, _Json_unwrap(value))
: (facts[key] = _Json_unwrap(value));

continue;
}

var subFacts = facts[tag] || (facts[tag] = {});
tag === "a3" && key === "class"
? _VirtualDom_addClass(subFacts, key, value)
: (subFacts[key] = value);
}

return facts;
}

function _VirtualDom_organizeFacts(factList) {
if (Array.isArray(factList)) {
return _VirtualDom_organizeFacts_usingArray(factList);
}

for (var facts = {}; factList.b; factList = factList.b) // WHILE_CONS
{
var entry = factList.a;

var tag = entry.$;
var key = entry.n;
var value = entry.o;

if (tag === "a2") {
key === "className"
? _VirtualDom_addClass(facts, key, _Json_unwrap(value))
: (facts[key] = _Json_unwrap(value));

continue;
}

var subFacts = facts[tag] || (facts[tag] = {});
tag === "a3" && key === "class"
? _VirtualDom_addClass(subFacts, key, value)
: (subFacts[key] = value);
}

return facts;
}
3 changes: 3 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { inlineNumberToString } from './transforms/inlineNumberToString';
import { reportFunctionStatusInBenchmarks, v8Debug } from './transforms/analyze';
import { recordUpdate } from './transforms/recordUpdate';
import * as Replace from './transforms/replace';
import {supportArraysForHtml, supportArraysForHtmlReplacements} from "./transforms/supportArraysForHtml";

export type Options = {
compile: boolean;
Expand Down Expand Up @@ -93,13 +94,15 @@ export const transform = async (
[transforms.fastCurriedFns, '/../replacements/faster-function-wrappers'],
[transforms.replaceListFunctions, '/../replacements/list'],
[transforms.replaceStringFunctions, '/../replacements/string'],
[transforms.arraysForHtml, supportArraysForHtmlReplacements],
]);

let inlineCtx: InlineContext | undefined;
const transformations: any[] = removeDisabled([
[transforms.replacements != null || replacements.length > 0, await Replace.fromFiles(transforms.replacements || {}, replacements)],
[transforms.v8Analysis, v8Debug],
[transforms.variantShapes, normalizeVariantShapes],
[transforms.arraysForHtml, supportArraysForHtml],
[transforms.inlineFunctions, createFunctionInlineTransformer(verbose, transforms.fastCurriedFns)],
[transforms.inlineEquality, inlineEquality()],
[transforms.inlineNumberToString, inlineNumberToString()],
Expand Down
127 changes: 127 additions & 0 deletions src/transforms/supportArraysForHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*

## Goal

This transformer aims to avoid unnecessary Elm List to JavaScript Array conversions when it comes to `elm/html` functions.

Here is a small example:

```elm
Html.div [ Html.Attributes.class "some", Html.Attributes.class "class" ] [ a, b, c ]
```
gets compiled to
```js
A2(
$elm$html$Html$div,
_List_fromArray([
$elm$html$Html$Attributes$class("some"),
$elm$html$Html$Attributes$class("class")
]),
_List_fromArray([a, b, c])
);
```

Through `_List_fromArray`, the attributes and children are converted from JavaScript Arrays to Elm Lists.
This conversion has a runtime cost, and Elm lists are slower to iterate through as well.

The idea behind this transformer is to remove these `_List_fromArray` calls and to keep the JavaScript Arrays as such,
and to have the underlying functions able to iterate through JavaScript Arrays as well. Taking the example from before,
the result would end up being:
```js
A2(
$elm$html$Html$div,
[
$elm$html$Html$Attributes$class("some"),
$elm$html$Html$Attributes$class("class")
],
[a, b, c]
);
```

This change will only be applied when the arguments are literal lists, not when they are variables or more complex expressions.
Further work could potentially increase the number of cases that we apply this change.

Elm Lists will have to be supported still, because this transformer will not be able to replace all attributes and children.

## Explanation of the transformer

1. When this transformer is enabled, replacements for `_VirtualDom_nodeNS` and `_VirtualDom_organizeFacts` are introduced.
These replacements will make the 2 functions (which are at the root of the VirtualDom functions) support JavaScript Arrays.
More replacements should probably be added to support SVG and custom nodes.
2. Detect which functions can benefit from this optimization. Currently, it's only the functions that call `_VirtualDom_node`,
but we could extend this further with a bit more analysis.
3. Remove `_List_fromArray` from the arguments from the functions detected in 2.

*/

import ts from 'typescript';
import {parseAXFunction} from "./utils/ElmWrappers";

export const supportArraysForHtmlReplacements = '/../replacements/virtual-dom';

export const supportArraysForHtml: ts.TransformerFactory<ts.SourceFile> = context => {
return sourceFile => {
const knownFunctionsToOptimize: Set<string> = new Set(['$elm$html$Html$node']);

const visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
if (ts.isVariableDeclaration(node)
&& ts.isIdentifier(node.name)
&& node.initializer
&& ts.isCallExpression(node.initializer)
&& ts.isIdentifier(node.initializer.expression)
&& node.initializer.expression.text === '_VirtualDom_node'
) {
knownFunctionsToOptimize.add(node.name.text);
return ts.visitEachChild(node, visitor, context);
}

if (ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& node.arguments.length > 0
) {
const arity = parseAXFunction(node.expression.text);
const functionName = arity ? getName(node.arguments[0]) : node.expression.text;

if (functionName && isOptimizableFunction(functionName, knownFunctionsToOptimize)) {
node = ts.factory.updateCallExpression(
node,
node.expression,
node.typeArguments,
node.arguments.map(removeListFromArray)
);
}
}
return ts.visitEachChild(node, visitor, context);
};

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

function removeListFromArray(node: ts.Expression): ts.Expression {
if (ts.isCallExpression(node)
&& ts.isIdentifier(node.expression)
&& node.expression.text === '_List_fromArray'
) {
return node.arguments[0];
}

if (ts.isIdentifier(node)
&& node.text === '_List_Nil'
) {
return ts.factory.createArrayLiteralExpression([]);
}

return node;
}

function getName(expr: ts.Expression): string | null {
if (ts.isIdentifier(expr)) {
return expr.text;
}
return null;
}

function isOptimizableFunction(functionName: string, knownFunctionsToOptimize: Set<string>): boolean {
return knownFunctionsToOptimize.has(functionName);
}
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type Transforms = {
variantShapes: boolean;
inlineNumberToString: boolean;
inlineEquality: boolean;
arraysForHtml: boolean;
inlineFunctions: boolean;
passUnwrappedFunctions: boolean;
listLiterals: InlineLists | false;
Expand Down Expand Up @@ -79,6 +80,7 @@ export const emptyOpts: Transforms = {
inlineNumberToString: false,
inlineFunctions: false,
inlineEquality: false,
arraysForHtml: false,
listLiterals: false,
passUnwrappedFunctions: false,
arrowFns: false,
Expand All @@ -100,6 +102,7 @@ export function toolDefaults(o3Enabled: boolean, replacements: { string: string
variantShapes: true,
inlineNumberToString: false,
inlineEquality: true,
arraysForHtml: true,
inlineFunctions: true,
listLiterals: false,
passUnwrappedFunctions: true,
Expand All @@ -123,6 +126,7 @@ export function benchmarkDefaults(o3Enabled: boolean, replacements: { string: st
variantShapes: true,
inlineNumberToString: false,
inlineEquality: true,
arraysForHtml: true,
inlineFunctions: true,
listLiterals: false,
passUnwrappedFunctions: true,
Expand Down Expand Up @@ -151,6 +155,7 @@ export const previous: Previous =
variantShapes: true,
inlineNumberToString: false,
inlineEquality: true,
arraysForHtml: true,
inlineFunctions: true,
listLiterals: false,
passUnwrappedFunctions: true,
Expand Down
Loading