diff --git a/bin/elm-optimize-level-2.js b/bin/elm-optimize-level-2.js old mode 100644 new mode 100755 diff --git a/src/transforms/recordUpdate.ts b/src/transforms/recordUpdate.ts index 90bbd37..9d42108 100644 --- a/src/transforms/recordUpdate.ts +++ b/src/transforms/recordUpdate.ts @@ -5,12 +5,17 @@ export const recordUpdate = (): ts.TransformerFactory => (context) => (sourceFile) => { const registry = new RecordRegistry(); - const propSet = new Set(); - const replacedUpdates = ts.visitNode(sourceFile, replaceUpdateStatements(propSet, context)); + const propSet = new Set(); + const updateSet = new Map(); + ts.visitNode(sourceFile, registerUpdateStatements(propSet, updateSet, context)); + + const replacedUpdates = ts.visitNode(sourceFile, replaceUpdateStatements(updateSet, context)); const replacedLiterals = ts.visitNode(replacedUpdates, replaceObjectLiterals(propSet, registry, context)); const recordStatements = createRecordStatements(registry); - const insertedCtors = ts.visitNode(replacedLiterals, insertRecordConstructors(recordStatements, context)); + const reusableUpdateStatements = createReusableUpdateStatements(updateSet); + const statementsToPrepend = recordStatements.concat(reusableUpdateStatements); + const insertedCtors = ts.visitNode(replacedLiterals, prependNodes(statementsToPrepend, context)); return insertedCtors; } @@ -18,7 +23,7 @@ export const recordUpdate = (): ts.TransformerFactory => class RecordRegistry { counter: number; - map: Map; + map: Map; constructor() { this.counter = 0; @@ -32,7 +37,7 @@ class RecordRegistry { const possibleShape = this.map.get(shapeId); if (possibleShape) { - return possibleShape.valueOf(); + return possibleShape; } const recordId = this.counter + 1; @@ -45,8 +50,54 @@ class RecordRegistry { } } +function registerUpdateStatements(propSet: Set, updateSet: Map, ctx: ts.TransformationContext) { + const visitorHelp = (node: ts.Node): ts.VisitResult => { + ts.visitEachChild(node, visitorHelp, ctx); -function replaceUpdateStatements(propSet: Set, ctx: ts.TransformationContext) { + const updateExpression = isUpdateExpression(node); + if (!updateExpression) { + return node; + } + + const objectLiteral = updateExpression.arguments[1] as ts.ObjectLiteralExpression; + const objectProperties = Array.from(objectLiteral.properties); + objectProperties.sort(objectLiteralPropertySort); + + // Add updated properties to propSet + + objectProperties. + map((it) => (it.name as ts.Identifier).text). + forEach((it) => { propSet.add(it); }); + + // Register how many times this particular update expression is being used + + const shape = objectProperties.map((it) => (it.name as ts.Identifier).text).join(','); + + let num = updateSet.get(shape) || 0; + num += 1 + updateSet.set(shape, num); + + return node; + } + + return visitorHelp; +} + +function objectLiteralPropertySort(a: ts.ObjectLiteralElementLike, b: ts.ObjectLiteralElementLike): number { + var nameA = (a.name as ts.Identifier).text; + var nameB = (b.name as ts.Identifier).text; + if (nameA < nameB) { + return -1; + } + + if (nameA > nameB) { + return 1; + } + + return 0; +} + +function replaceUpdateStatements(updateSet: Map, ctx: ts.TransformationContext) { const visitorHelp = (node: ts.Node): ts.VisitResult => { const visitedNode = ts.visitEachChild(node, visitorHelp, ctx); const updateExpression = isUpdateExpression(visitedNode); @@ -55,52 +106,19 @@ function replaceUpdateStatements(propSet: Set, ctx: ts.TransformationCon } const objName = (updateExpression.arguments[0] as ts.Identifier).text; - const copyId = ts.createIdentifier('$r'); - const cloneObj = ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList([ - ts.createVariableDeclaration( - copyId, - undefined, - ts.createCall( - ts.createPropertyAccess( - ts.createIdentifier(objName), - ts.createIdentifier('$clone') - ), - undefined, - [] - ) - ) - ]) - ); - - // Add updated properties to propSet const objectLiteral = updateExpression.arguments[1] as ts.ObjectLiteralExpression; + const objectProperties = Array.from(objectLiteral.properties); + objectProperties.sort(objectLiteralPropertySort); - objectLiteral.properties. - map((it) => (it.name as ts.Identifier).text). - forEach((it) => { propSet.add(it); }); + const updateShape = objectProperties.map((it) => (it.name as ts.Identifier).text).join(','); + const updatePerformed = updateSet.get(updateShape) || 0; - const propSetters: ts.Statement[] = objectLiteral.properties. - map((it) => ts.createExpressionStatement( - ts.createBinary( - ts.createPropertyAccess( - copyId, - it.name as ts.Identifier - ), - ts.createToken(ts.SyntaxKind.EqualsToken), - (it as ts.PropertyAssignment).initializer - ) - )); + if (updatePerformed > 1) { + return generateCodeForReusableUpdate(objName, updateShape, objectProperties); + } - const retStmt = ts.createReturn(copyId); - - propSetters.push(retStmt); - - const block = [ cloneObj as ts.Statement ].concat(propSetters); - - return ts.createImmediatelyInvokedFunctionExpression(block); + return generateCodeForSingleUpdate(objName, objectProperties); } return visitorHelp; @@ -116,9 +134,69 @@ function isUpdateExpression(node: ts.Node): ts.CallExpression | null { return null } +function generateCodeForReusableUpdate(objName: string, shape: string, objectProperties: Array): ts.Node { + const updateFnName = `$$update__${shape.replaceAll(',', '__')}`; -function replaceObjectLiterals(propSet: Set, registry: RecordRegistry, ctx: ts.TransformationContext) { + const initialArgs: Array = [ ts.createIdentifier(objName) ]; + const newValues = objectProperties.map((it) => (it as ts.PropertyAssignment).initializer); + const args = initialArgs.concat(newValues); + + return ts.createCall( + ts.createIdentifier(updateFnName), + undefined, + args + ); +} + +function generateCodeForSingleUpdate(objName: string, objectProperties: Array): ts.Node { + const copyId = ts.createIdentifier('$r'); + + const cloneObj = ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList([ + ts.createVariableDeclaration( + copyId, + undefined, + ts.createCall( + ts.createPropertyAccess( + ts.createIdentifier(objName), + ts.createIdentifier('$c') + ), + undefined, + [] + ) + ) + ]) + ); + + const propSetters: ts.Statement[] = objectProperties. + map((it) => ts.createExpressionStatement( + ts.createBinary( + ts.createPropertyAccess( + copyId, + it.name as ts.Identifier + ), + ts.createToken(ts.SyntaxKind.EqualsToken), + (it as ts.PropertyAssignment).initializer + ) + )); + + const retStmt = ts.createReturn(copyId); + + propSetters.push(retStmt); + + const block = [ cloneObj as ts.Statement ].concat(propSetters); + + return ts.createImmediatelyInvokedFunctionExpression(block); +} + + +function replaceObjectLiterals(propSet: Set, registry: RecordRegistry, ctx: ts.TransformationContext) { const visitorHelp = (node: ts.Node): ts.VisitResult => { + if (isTupleConstructor(node)) { + return node; + } + const visitedNode = ts.visitEachChild(node, visitorHelp, ctx); const objectLiteral = isRecordLiteral(visitedNode); if (!objectLiteral) { @@ -146,6 +224,11 @@ function replaceObjectLiterals(propSet: Set, registry: RecordRegistry, c return visitorHelp; } +function isTupleConstructor(node: ts.Node): boolean { + return ts.isFunctionDeclaration(node) && + (node.name?.text === '_Utils_Tuple2' || node.name?.text === '_Utils_Tuple3'); +} + function isRecordLiteral(node: ts.Node): ts.ObjectLiteralExpression | null { if (ts.isObjectLiteralExpression(node) && node.properties[0] && (node.properties[0]?.name as ts.Identifier)?.text !== '$') { return node as ts.ObjectLiteralExpression; @@ -179,17 +262,46 @@ function createRecordStatement(className: string, props: string[]): string { ${propSetters} } - ${className}.prototype.$clone = function() { + ${className}.prototype.$c = function() { return new ${className}(${propGetters}); } `; } -function insertRecordConstructors(ctors: ts.Node[], ctx: ts.TransformationContext) { +function createReusableUpdateStatements(updateSet: Map): ts.Node[] { + const statementString = Array.from(updateSet.entries()). + filter((it) => it[1] > 1). + map((it) => createReusableUpdateStatement(it[0])). + join('\n'); + + return astNodes(statementString); +} + +function createReusableUpdateStatement(shape: string): string { + const updateFnName = `$$update__${shape.replaceAll(',', '__')}`; + + const props = shape.split(','); + const propSetters = props. + map((name) => `$r.${name} = ${name};`). + join(' '); + + const propList = [ 'obj' ].concat(props).join(','); + + return ` + function ${updateFnName}(${propList}) { + var $r = obj.$c(); + ${propSetters} + return $r; + } + `; +} + + +function prependNodes(nodes: ts.Node[], ctx: ts.TransformationContext) { const visitorHelp = (node: ts.Node): ts.VisitResult => { if (isFirstFWrapper(node)) { - return ctors.concat(node); + return nodes.concat(node); } return ts.visitEachChild(node, visitorHelp, ctx);