From 1c71424ecacf22415b38068121bc8b4fcd688570 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Mon, 27 Dec 2021 23:07:00 +0100 Subject: [PATCH 001/229] Add base tests --- src/transforms/tailCallRecursion.ts | 40 +++++++++++++ test/tailCallRecursion.test.ts | 88 +++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/transforms/tailCallRecursion.ts create mode 100644 test/tailCallRecursion.test.ts diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts new file mode 100644 index 00000000..6afd78da --- /dev/null +++ b/src/transforms/tailCallRecursion.ts @@ -0,0 +1,40 @@ +import ts from 'typescript'; + +/* + +Applies tail-call recursion when possible, where the compiler didn't. + +This function gets tail-call optimized. + + tco : (a -> b) -> List a -> List b -> List b + tco mapper list acc = + case list of + [] -> + acc + + x :: xs -> + tco mapper xs (mapper x :: acc) + +but this version doesn't (because of the additional `<|`): + + nonTco : (a -> b) -> List a -> List b -> List b + nonTco mapper list acc = + case list of + [] -> + acc + + x :: xs -> + nonTco mapper xs <| (mapper x :: acc) + +*/ + +export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : any) => { + return (sourceFile) => { + const visitor = (originalNode: ts.Node): ts.VisitResult => { + const node = ts.visitEachChild(originalNode, visitor, context); + return node; + }; + + return ts.visitNode(sourceFile, visitor); + }; +}; diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts new file mode 100644 index 00000000..504594c7 --- /dev/null +++ b/test/tailCallRecursion.test.ts @@ -0,0 +1,88 @@ +import ts from 'typescript'; + +import { createTailCallRecursionTransformer } from '../src/transforms/tailCallRecursion'; + +test('it can turn a function that is tail-call recursive into a while loop', () => { + const initialCode = ` + var something$recursiveFunction = F3( + function (mapper, list, acc) { + if (!list.b) { + return acc; + } else { + var x = list.a; + var xs = list.b; + return A3( + something$recursiveFunction, + mapper, + xs, + A2( + $elm$core$List$cons, + mapper(x), + acc)); + } + }); + `; + + const expectedOutputCode = ` + var something$recursiveFunction = F3( + function (mapper, list, acc) { + recursiveFunction: + while (true) { + if (!list.b) { + return acc; + } else { + var x = list.a; + var xs = list.b; + var $temp$mapper = mapper, + $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + mapper = $temp$mapper; + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + +export function transformCode( + initialCode: string, + expectedCode: string, + transformer: ts.TransformerFactory +): { + actual: string; + expected: string; +} { + const source = ts.createSourceFile( + 'elm.js', + initialCode, + ts.ScriptTarget.ES2018 + ); + + const printer = ts.createPrinter(); + + const [output] = ts.transform(source, [transformer]).transformed; + + const expectedOutput = printer.printFile( + ts.createSourceFile('elm.js', expectedCode, ts.ScriptTarget.ES2018) + ); + + const printedOutput = printer.printFile(output); + + return { + actual: printedOutput, + expected: expectedOutput, + }; +} From 6934b797e9c59685d66f9c39ac85d5d5afeef1a9 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Tue, 28 Dec 2021 11:23:54 +0100 Subject: [PATCH 002/229] Add passing test --- test/tailCallRecursion.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 504594c7..30288df9 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -2,6 +2,22 @@ import ts from 'typescript'; import { createTailCallRecursionTransformer } from '../src/transforms/tailCallRecursion'; +test("it doesn't affect functions that are not recursive", () => { + const initialCode = ` + var $elm$core$Basics$identity = function (x) { + return x; + }; + `; + + const { actual, expected } = transformCode( + initialCode, + initialCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + test('it can turn a function that is tail-call recursive into a while loop', () => { const initialCode = ` var something$recursiveFunction = F3( From 235731d8a0d6091a0131abae728c078a06d2c37a Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Tue, 28 Dec 2021 11:24:47 +0100 Subject: [PATCH 003/229] Add passing test --- test/tailCallRecursion.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 30288df9..f850500c 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -18,6 +18,22 @@ test("it doesn't affect functions that are not recursive", () => { expect(actual).toBe(expected); }); +test("it doesn't affect functions that are not tail-call recursive", () => { + const initialCode = ` + var factorial = function (n) { + return mult(n, factorial(n - 1)); + }; + `; + + const { actual, expected } = transformCode( + initialCode, + initialCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + test('it can turn a function that is tail-call recursive into a while loop', () => { const initialCode = ` var something$recursiveFunction = F3( From 4883fff5bfa5e4fa8a71d08b5d1deaa0184cee88 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Tue, 28 Dec 2021 11:29:25 +0100 Subject: [PATCH 004/229] Add TODO --- src/transforms/tailCallRecursion.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 6afd78da..36740af5 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -28,6 +28,12 @@ but this version doesn't (because of the additional `<|`): */ +// TODO Enable TCO when it should have been enabled but not triggered because of `<|` or `|>` +// TODO Re-use the existing loop and goto label if there is already one +// TODO Enable TCO for tail recursion modulo cons +// TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` +// TODO Enable TCO for other kinds of data constructors + export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : any) => { return (sourceFile) => { const visitor = (originalNode: ts.Node): ts.VisitResult => { From a78d6169a9d68ac41fb55646cafa418b2a425e5a Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Tue, 28 Dec 2021 11:29:30 +0100 Subject: [PATCH 005/229] Document the original source code --- test/tailCallRecursion.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index f850500c..711e7308 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -35,6 +35,14 @@ test("it doesn't affect functions that are not tail-call recursive", () => { }); test('it can turn a function that is tail-call recursive into a while loop', () => { + // Corresponds to the following Elm code + // recursiveFunction : (a -> b) -> List a -> List b -> List b + // recursiveFunction mapper list acc = + // case list of + // [] -> + // acc + // x :: xs -> + // recursiveFunction mapper xs <| (mapper x :: acc) const initialCode = ` var something$recursiveFunction = F3( function (mapper, list, acc) { @@ -55,6 +63,14 @@ test('it can turn a function that is tail-call recursive into a while loop', () }); `; + // Corresponds to the following TCO-ed Elm code + // recursiveFunction : (a -> b) -> List a -> List b -> List b + // recursiveFunction mapper list acc = + // case list of + // [] -> + // acc + // x :: xs -> + // recursiveFunction mapper xs (mapper x :: acc) const expectedOutputCode = ` var something$recursiveFunction = F3( function (mapper, list, acc) { From 0070b32857baec3720cb0a693dede20f28486585 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 11:54:31 +0100 Subject: [PATCH 006/229] Add forTests parameter --- src/transforms/tailCallRecursion.ts | 2 +- test/tailCallRecursion.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 36740af5..4a8f0aad 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -34,7 +34,7 @@ but this version doesn't (because of the additional `<|`): // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors -export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : any) => { +export const createTailCallRecursionTransformer = (forTests: boolean): ts.TransformerFactory => (context) => { return (sourceFile) => { const visitor = (originalNode: ts.Node): ts.VisitResult => { const node = ts.visitEachChild(originalNode, visitor, context); diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 711e7308..f0b301c9 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -108,7 +108,7 @@ test('it can turn a function that is tail-call recursive into a while loop', () export function transformCode( initialCode: string, expectedCode: string, - transformer: ts.TransformerFactory + transformer: (forTests: boolean) => ts.TransformerFactory ): { actual: string; expected: string; @@ -121,7 +121,7 @@ export function transformCode( const printer = ts.createPrinter(); - const [output] = ts.transform(source, [transformer]).transformed; + const [output] = ts.transform(source, [transformer(true)]).transformed; const expectedOutput = printer.printFile( ts.createSourceFile('elm.js', expectedCode, ts.ScriptTarget.ES2018) From 5f4c8268ecc75199cccd54c794aaaad2ab295f9e Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 11:56:49 +0100 Subject: [PATCH 007/229] DEBUG --- src/transforms/tailCallRecursion.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 4a8f0aad..528c70b1 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -40,6 +40,7 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf const node = ts.visitEachChild(originalNode, visitor, context); return node; }; + console.log(forTests); return ts.visitNode(sourceFile, visitor); }; From c2860cb819809464a773ebba8b77a1331922711b Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 11:57:25 +0100 Subject: [PATCH 008/229] Visit node afterwards --- src/transforms/tailCallRecursion.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 528c70b1..578c759a 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -36,9 +36,8 @@ but this version doesn't (because of the additional `<|`): export const createTailCallRecursionTransformer = (forTests: boolean): ts.TransformerFactory => (context) => { return (sourceFile) => { - const visitor = (originalNode: ts.Node): ts.VisitResult => { - const node = ts.visitEachChild(originalNode, visitor, context); - return node; + const visitor = (node: ts.Node): ts.VisitResult => { + return ts.visitEachChild(node, visitor, context); }; console.log(forTests); From 10456a1ed07ce96c0defc69ed0434740779b473c Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 11:59:10 +0100 Subject: [PATCH 009/229] Add condition with dummy replacement --- src/transforms/tailCallRecursion.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 578c759a..cd4ce636 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -37,6 +37,11 @@ but this version doesn't (because of the additional `<|`): export const createTailCallRecursionTransformer = (forTests: boolean): ts.TransformerFactory => (context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { + if (ts.isVariableDeclaration(node) + && node.initializer + && ts.isCallExpression(node.initializer)) { + return ts.createLiteral("ok"); + } return ts.visitEachChild(node, visitor, context); }; console.log(forTests); From bccaa66680f568076bc5e946994c9e73d37b919d Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 12:05:54 +0100 Subject: [PATCH 010/229] Add condition for F call --- src/transforms/tailCallRecursion.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index cd4ce636..2192cbc6 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -39,7 +39,7 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) && node.initializer - && ts.isCallExpression(node.initializer)) { + && isFCall(node.initializer)) { return ts.createLiteral("ok"); } return ts.visitEachChild(node, visitor, context); @@ -49,3 +49,9 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf return ts.visitNode(sourceFile, visitor); }; }; + +function isFCall(node: ts.Expression): boolean { + return ts.isCallExpression(node) + && ts.isIdentifier(node.expression) + && node.expression.text.startsWith('F'); +} \ No newline at end of file From 606b8dc02b446d499a93d641ba9a8217034d0122 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 12:07:34 +0100 Subject: [PATCH 011/229] Return expression node --- src/transforms/tailCallRecursion.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 2192cbc6..4b1be8b7 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -38,9 +38,11 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) - && node.initializer - && isFCall(node.initializer)) { - return ts.createLiteral("ok"); + && node.initializer) { + const fn = isFCall(node.initializer); + if (fn) { + return ts.createLiteral("ok"); + } } return ts.visitEachChild(node, visitor, context); }; @@ -50,8 +52,12 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf }; }; -function isFCall(node: ts.Expression): boolean { - return ts.isCallExpression(node) +function isFCall(node: ts.Expression): ts.Expression | null { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) - && node.expression.text.startsWith('F'); + && node.expression.text.startsWith('F')) { + return node.arguments[0]; + } + + return null; } \ No newline at end of file From b8a90fe810b8934494e0f8d36e5fcdc8873b192f Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 12:09:07 +0100 Subject: [PATCH 012/229] Return node itself --- src/transforms/tailCallRecursion.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 4b1be8b7..8f6caa01 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -41,7 +41,12 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && node.initializer) { const fn = isFCall(node.initializer); if (fn) { - return ts.createLiteral("ok"); + return ts.updateVariableDeclaration( + node, + node.name, + undefined, + node.initializer + ); } } return ts.visitEachChild(node, visitor, context); From 5abe87e1528e2e240c2679ea090da5595f0918e4 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 12:12:54 +0100 Subject: [PATCH 013/229] Re-create initializer --- src/transforms/tailCallRecursion.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 8f6caa01..4aafb89a 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -38,14 +38,22 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) - && node.initializer) { + && node.initializer + && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { + const initializer = ts.updateCall( + node.initializer, + node.initializer.expression, + undefined, + [fn] + ); + return ts.updateVariableDeclaration( - node, - node.name, - undefined, - node.initializer + node, + node.name, + undefined, + initializer ); } } @@ -57,9 +65,8 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf }; }; -function isFCall(node: ts.Expression): ts.Expression | null { - if (ts.isCallExpression(node) - && ts.isIdentifier(node.expression) +function isFCall(node: ts.CallExpression): ts.Expression | null { + if (ts.isIdentifier(node.expression) && node.expression.text.startsWith('F')) { return node.arguments[0]; } From af905e69915d93d8f6ff65e5afdcc526979e1623 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 14:40:56 +0100 Subject: [PATCH 014/229] Collect name --- src/transforms/tailCallRecursion.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 4aafb89a..ecf372c5 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -42,6 +42,9 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { + if (ts.isIdentifier(node.name)) { + console.log(node.name.text) + } const initializer = ts.updateCall( node.initializer, node.initializer.expression, From af1060d5b5363b0234af2364ae0fdae386e7481d Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 14:45:01 +0100 Subject: [PATCH 015/229] Move condition --- src/transforms/tailCallRecursion.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index ecf372c5..75f7d273 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -38,13 +38,12 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) + && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { - if (ts.isIdentifier(node.name)) { - console.log(node.name.text) - } + console.log(node.name.text) const initializer = ts.updateCall( node.initializer, node.initializer.expression, From 0ddbcb7e79a4ca7b9b1b9f71d8983cd10dde178b Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 14:52:46 +0100 Subject: [PATCH 016/229] Update fn --- src/transforms/tailCallRecursion.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 75f7d273..e4471dce 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -43,12 +43,22 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { + const newFn = ts.createFunctionExpression( + fn.modifiers, + undefined, + fn.name, + undefined, + fn.parameters, + undefined, + fn.body + ); + console.log(node.name.text) const initializer = ts.updateCall( node.initializer, node.initializer.expression, undefined, - [fn] + [newFn] ); return ts.updateVariableDeclaration( @@ -67,10 +77,17 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf }; }; -function isFCall(node: ts.CallExpression): ts.Expression | null { +function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { if (ts.isIdentifier(node.expression) - && node.expression.text.startsWith('F')) { - return node.arguments[0]; + && node.expression.text.startsWith('F') + && node.arguments.length > 0 + ) { + const fn = node.arguments[0]; + if (ts.isFunctionExpression(fn)) { + return fn; + } + + return null; } return null; From d82c81e6fbb48b9073492a624e8e460d95a2f857 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 14:56:38 +0100 Subject: [PATCH 017/229] Add function --- src/transforms/tailCallRecursion.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index e4471dce..1350766e 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -43,6 +43,7 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { + const newBody = updateFunctionBody(fn.body); const newFn = ts.createFunctionExpression( fn.modifiers, undefined, @@ -91,4 +92,8 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { } return null; +} + +function updateFunctionBody(body : ts.Block) : ts.Block { + return body; } \ No newline at end of file From d979ad7769c7f9f746e27f76d1bec21a20f3fe36 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 14:57:09 +0100 Subject: [PATCH 018/229] Pass function name --- src/transforms/tailCallRecursion.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 1350766e..f7a18890 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -43,7 +43,7 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { - const newBody = updateFunctionBody(fn.body); + const newBody = updateFunctionBody(node.name.text, fn.body); const newFn = ts.createFunctionExpression( fn.modifiers, undefined, @@ -54,7 +54,6 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf fn.body ); - console.log(node.name.text) const initializer = ts.updateCall( node.initializer, node.initializer.expression, @@ -94,6 +93,7 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { return null; } -function updateFunctionBody(body : ts.Block) : ts.Block { +function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { + console.log(functionName) return body; } \ No newline at end of file From 5b4c03d8c2c5bd10e67d0deb65b256efc5a79e60 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:01:00 +0100 Subject: [PATCH 019/229] Add label and while loop --- src/transforms/tailCallRecursion.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f7a18890..8ece208e 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -94,6 +94,7 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { } function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { - console.log(functionName) - return body; + return ts.createBlock( + [ts.createLabel(functionName, ts.createWhile(ts.createTrue(), body))] + ); } \ No newline at end of file From 592f300e18be76ec7885f9aa4f655e9c1d966708 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:01:08 +0100 Subject: [PATCH 020/229] Use new body --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 8ece208e..35451812 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -51,7 +51,7 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf undefined, fn.parameters, undefined, - fn.body + newBody ); const initializer = ts.updateCall( From 2c76128a520ddbf75c38580ac0548b953ba21e57 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:06:02 +0100 Subject: [PATCH 021/229] Use the label --- src/transforms/tailCallRecursion.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 35451812..3c1eb80b 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -94,7 +94,9 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { } function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { + const labelSplits = functionName.split("$"); + const label = labelSplits[labelSplits.length - 1] || functionName; return ts.createBlock( - [ts.createLabel(functionName, ts.createWhile(ts.createTrue(), body))] + [ts.createLabel(label, ts.createWhile(ts.createTrue(), body))] ); } \ No newline at end of file From 7b8fdf52d8d45301cb2995c4a7ef50c2d83a9b22 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:07:18 +0100 Subject: [PATCH 022/229] Formatting --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 3c1eb80b..4262c093 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -99,4 +99,4 @@ function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { return ts.createBlock( [ts.createLabel(label, ts.createWhile(ts.createTrue(), body))] ); -} \ No newline at end of file +} From 31290bd1767cf80258ee10e6c89b5e658c836019 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:10:21 +0100 Subject: [PATCH 023/229] Update calls --- src/transforms/tailCallRecursion.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 4262c093..61b24906 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -97,6 +97,10 @@ function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; return ts.createBlock( - [ts.createLabel(label, ts.createWhile(ts.createTrue(), body))] + [ts.createLabel(label, ts.createWhile(ts.createTrue(), updateRecursiveCalls(body)))] ); } + +function updateRecursiveCalls(node : ts.Statement) : ts.Statement { + return node; +} \ No newline at end of file From 4d524677356f56abbb76fa91747388d7128ec152 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:11:35 +0100 Subject: [PATCH 024/229] Extract variable --- src/transforms/tailCallRecursion.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 61b24906..c439f1b3 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -96,8 +96,10 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; + const updatedBlock = updateRecursiveCalls(body); + return ts.createBlock( - [ts.createLabel(label, ts.createWhile(ts.createTrue(), updateRecursiveCalls(body)))] + [ts.createLabel(label, ts.createWhile(ts.createTrue(), updatedBlock))] ); } From 8487025f5809d5d0219db42a782d7679950ad84a Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:14:22 +0100 Subject: [PATCH 025/229] Use visitor --- src/transforms/tailCallRecursion.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index c439f1b3..dd37fa0c 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -34,7 +34,9 @@ but this version doesn't (because of the additional `<|`): // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors -export const createTailCallRecursionTransformer = (forTests: boolean): ts.TransformerFactory => (context) => { +type Context = any; + +export const createTailCallRecursionTransformer = (forTests: boolean): ts.TransformerFactory => (context : Context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) @@ -43,7 +45,7 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { - const newBody = updateFunctionBody(node.name.text, fn.body); + const newBody = updateFunctionBody(node.name.text, fn.body, context); const newFn = ts.createFunctionExpression( fn.modifiers, undefined, @@ -93,16 +95,17 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { return null; } -function updateFunctionBody(functionName : string, body : ts.Block) : ts.Block { +function updateFunctionBody(functionName : string, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; - const updatedBlock = updateRecursiveCalls(body); + const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); return ts.createBlock( [ts.createLabel(label, ts.createWhile(ts.createTrue(), updatedBlock))] ); } -function updateRecursiveCalls(node : ts.Statement) : ts.Statement { + +function updateRecursiveCallVisitor(node: ts.Node): ts.VisitResult { return node; } \ No newline at end of file From c6f3e70dbc294bf62f707eb0a677cdf3c38db7df Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:15:36 +0100 Subject: [PATCH 026/229] Visit on --- src/transforms/tailCallRecursion.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index dd37fa0c..5f40dec3 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -100,12 +100,14 @@ function updateFunctionBody(functionName : string, body : ts.Block, context : Co const label = labelSplits[labelSplits.length - 1] || functionName; const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); + function updateRecursiveCallVisitor(node: ts.Node): ts.VisitResult { + if (ts.isBlock(node)) { + return ts.visitEachChild(body, updateRecursiveCallVisitor, context); + } + return node; + } + return ts.createBlock( [ts.createLabel(label, ts.createWhile(ts.createTrue(), updatedBlock))] ); -} - - -function updateRecursiveCallVisitor(node: ts.Node): ts.VisitResult { - return node; } \ No newline at end of file From 5d87fe827ce53e9e9b061269bf6b4c9751c4512d Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:17:58 +0100 Subject: [PATCH 027/229] Support if --- src/transforms/tailCallRecursion.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 5f40dec3..662c1ff7 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -104,6 +104,16 @@ function updateFunctionBody(functionName : string, body : ts.Block, context : Co if (ts.isBlock(node)) { return ts.visitEachChild(body, updateRecursiveCallVisitor, context); } + + if (ts.isIfStatement(node)) { + return ts.updateIf( + node, + node.expression, + ts.visitNode(node.thenStatement, updateRecursiveCallVisitor, context), + ts.visitNode(node.elseStatement, updateRecursiveCallVisitor, context) + ) + } + return node; } From 7ddbb62c957ef007358e8eab90f3368fd17788b6 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:36:18 +0100 Subject: [PATCH 028/229] Try and extract arguments --- src/transforms/tailCallRecursion.ts | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 662c1ff7..d8bfdb94 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -102,7 +102,7 @@ function updateFunctionBody(functionName : string, body : ts.Block, context : Co function updateRecursiveCallVisitor(node: ts.Node): ts.VisitResult { if (ts.isBlock(node)) { - return ts.visitEachChild(body, updateRecursiveCallVisitor, context); + return ts.visitEachChild(node, updateRecursiveCallVisitor, context); } if (ts.isIfStatement(node)) { @@ -114,10 +114,42 @@ function updateFunctionBody(functionName : string, body : ts.Block, context : Co ) } + if (ts.isReturnStatement(node) + && node.expression + && ts.isCallExpression(node.expression) + ) { + const newArguments = extractCallTo(functionName, node.expression); + return [ + ts.createContinue(label) + ]; + } + return node; } return ts.createBlock( [ts.createLabel(label, ts.createWhile(ts.createTrue(), updatedBlock))] ); +} + +function extractCallTo(functionName : string, node : ts.CallExpression) : Array | null { + if (!ts.isIdentifier(node.expression)) { + return null; + } + + // Is "fn(...)" + if (node.expression.text === functionName) { + return [...node.arguments]; + } + + // Is "AX(fn, ...)" + const firstArg = node.arguments[0]; + if (node.expression.text.startsWith("A") + && ts.isIdentifier(firstArg) + && firstArg.text === functionName + ) { + return node.arguments.slice(1); + } + + return null; } \ No newline at end of file From c19d986bfce6223687bb5ce381247cb242501d19 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:39:49 +0100 Subject: [PATCH 029/229] Pass parameter names --- src/transforms/tailCallRecursion.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index d8bfdb94..5811f6ae 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -45,7 +45,10 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf && ts.isCallExpression(node.initializer)) { const fn = isFCall(node.initializer); if (fn) { - const newBody = updateFunctionBody(node.name.text, fn.body, context); + const parameterNames : Array = fn.parameters.map(param => { + return ts.isIdentifier(param.name) ? param.name.text : ''; + }); + const newBody = updateFunctionBody(node.name.text, parameterNames, fn.body, context); const newFn = ts.createFunctionExpression( fn.modifiers, undefined, @@ -95,7 +98,7 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { return null; } -function updateFunctionBody(functionName : string, body : ts.Block, context : Context) : ts.Block { +function updateFunctionBody(functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); From 718b6804724e1c1805bad7fa03bc529be4173dd3 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:42:59 +0100 Subject: [PATCH 030/229] Assign parameters --- src/transforms/tailCallRecursion.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 5811f6ae..190631ff 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -122,7 +122,13 @@ function updateFunctionBody(functionName : string, parameterNames : Array + ts.createAssignment(ts.createIdentifier(name), newArguments[index]) + ), ts.createContinue(label) ]; } From c4ddd60fac78176fa05d348f6d81393fa21360ee Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:43:55 +0100 Subject: [PATCH 031/229] Remove forTests --- src/transforms/tailCallRecursion.ts | 3 +-- test/tailCallRecursion.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 190631ff..839f66f5 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -36,7 +36,7 @@ but this version doesn't (because of the additional `<|`): type Context = any; -export const createTailCallRecursionTransformer = (forTests: boolean): ts.TransformerFactory => (context : Context) => { +export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : Context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) @@ -76,7 +76,6 @@ export const createTailCallRecursionTransformer = (forTests: boolean): ts.Transf } return ts.visitEachChild(node, visitor, context); }; - console.log(forTests); return ts.visitNode(sourceFile, visitor); }; diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index f0b301c9..711e7308 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -108,7 +108,7 @@ test('it can turn a function that is tail-call recursive into a while loop', () export function transformCode( initialCode: string, expectedCode: string, - transformer: (forTests: boolean) => ts.TransformerFactory + transformer: ts.TransformerFactory ): { actual: string; expected: string; @@ -121,7 +121,7 @@ export function transformCode( const printer = ts.createPrinter(); - const [output] = ts.transform(source, [transformer(true)]).transformed; + const [output] = ts.transform(source, [transformer]).transformed; const expectedOutput = printer.printFile( ts.createSourceFile('elm.js', expectedCode, ts.ScriptTarget.ES2018) From 92e91e3230f65aa1aea2829e445d2ea4597ae106 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 15:47:05 +0100 Subject: [PATCH 032/229] Add temporary variables --- src/transforms/tailCallRecursion.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 839f66f5..f7e1baea 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -125,8 +125,17 @@ function updateFunctionBody(functionName : string, parameterNames : Array - ts.createAssignment(ts.createIdentifier(name), newArguments[index]) + ts.createVariableDeclarationList( + parameterNames.map((name, index) => + ts.createVariableDeclaration( + `$temp$${name}`, + undefined, + newArguments[index] + ) + ) + ), + ...parameterNames.map(name => + ts.createAssignment(ts.createIdentifier(name), ts.createIdentifier(`$temp$${name}`)) ), ts.createContinue(label) ]; From b154f428c801ccd65abcff2118392372a971c42b Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 18:45:55 +0100 Subject: [PATCH 033/229] Enable tail call recursion in the transform --- src/transform.ts | 2 ++ src/types.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/transform.ts b/src/transform.ts index 36dcc222..cd5eec96 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -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 { createTailCallRecursionTransformer } from './transforms/tailCallRecursion'; export type Options = { compile: boolean; @@ -87,6 +88,7 @@ export const transform = async ( let inlineCtx: InlineContext | undefined; const transformations: any[] = removeDisabled([ [transforms.replacements != null, replacementTransformer ], + [transforms.tailCallRecursion, createTailCallRecursionTransformer ], [transforms.fastCurriedFns, Replace.from_file('/../replacements/faster-function-wrappers') ], [transforms.replaceListFunctions, Replace.from_file('/../replacements/list') ], [transforms.replaceStringFunctions, Replace.from_file('/../replacements/string') ], diff --git a/src/types.ts b/src/types.ts index e383dd52..c50c6ff2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,7 @@ export type Transforms = { recordUpdates: boolean; v8Analysis: boolean; fastCurriedFns: boolean; + tailCallRecursion: boolean; replacements: { [name: string]: string } | null }; @@ -93,6 +94,7 @@ export const emptyOpts: Transforms = { recordUpdates: false, v8Analysis: false, fastCurriedFns: false, + tailCallRecursion: true, replacements: null }; @@ -115,6 +117,7 @@ export function toolDefaults(o3Enabled: boolean, replacements: { string: string recordUpdates: o3Enabled, v8Analysis: false, fastCurriedFns: true, + tailCallRecursion: true, replacements: replacements }; } @@ -138,6 +141,7 @@ export function benchmarkDefaults(o3Enabled: boolean, replacements: { string: st recordUpdates: o3Enabled, v8Analysis: false, fastCurriedFns: true, + tailCallRecursion: true, replacements: replacements }; } @@ -166,6 +170,7 @@ export const previous: Previous = recordUpdates: false, v8Analysis: false, fastCurriedFns: false, + tailCallRecursion: true, replacements: null } } From 98349f2ad6bc96da53ce3703421716b59f37c555 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 18:55:53 +0100 Subject: [PATCH 034/229] Update TODO --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f7e1baea..0c423cb6 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -28,7 +28,7 @@ but this version doesn't (because of the additional `<|`): */ -// TODO Enable TCO when it should have been enabled but not triggered because of `<|` or `|>` +// TODO Don't update non-TCO-able recursive functions // TODO Re-use the existing loop and goto label if there is already one // TODO Enable TCO for tail recursion modulo cons // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` From 3c11216786cbefc91f9c8d9777def4fbb77b76ff Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:13:21 +0100 Subject: [PATCH 035/229] Make sure non-recursive functions are not touched --- src/transforms/tailCallRecursion.ts | 12 +++++++++--- test/tailCallRecursion.test.ts | 30 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 0c423cb6..d1d065b7 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -28,7 +28,6 @@ but this version doesn't (because of the additional `<|`): */ -// TODO Don't update non-TCO-able recursive functions // TODO Re-use the existing loop and goto label if there is already one // TODO Enable TCO for tail recursion modulo cons // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` @@ -38,6 +37,8 @@ type Context = any; export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : Context) => { return (sourceFile) => { + const functionsToBeMadeRecursive : Record = {}; + const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) @@ -48,7 +49,10 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory = fn.parameters.map(param => { return ts.isIdentifier(param.name) ? param.name.text : ''; }); - const newBody = updateFunctionBody(node.name.text, parameterNames, fn.body, context); + const newBody = updateFunctionBody(functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); + if (!functionsToBeMadeRecursive[node.name.text]) { + return node; + } const newFn = ts.createFunctionExpression( fn.modifiers, undefined, @@ -97,7 +101,7 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { return null; } -function updateFunctionBody(functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { +function updateFunctionBody(functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); @@ -124,6 +128,8 @@ function updateFunctionBody(functionName : string, parameterNames : Array diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 711e7308..f59b8ce5 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -105,6 +105,36 @@ test('it can turn a function that is tail-call recursive into a while loop', () expect(actual).toBe(expected); }); +test('should not change non-recursive functions', () => { + const initialCode = ` + var something$recursiveFunction = F3( + function (mapper, list, acc) { + if (!list.b) { + return acc; + } else { + var x = list.a; + var xs = list.b; + return A3( + something$else, + mapper, + xs, + A2( + $elm$core$List$cons, + mapper(x), + acc)); + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + initialCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + export function transformCode( initialCode: string, expectedCode: string, From d0c65458db549423ed47529bcbd0302a1a34028b Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:17:26 +0100 Subject: [PATCH 036/229] Add failing test --- test/tailCallRecursion.test.ts | 101 +++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index f59b8ce5..1e7ec1de 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -105,6 +105,107 @@ test('it can turn a function that is tail-call recursive into a while loop', () expect(actual).toBe(expected); }); +test('should re-use the label and while loop if there already is one', () => { + // Corresponds to the following Elm code + // recursiveFunction : (a -> b) -> List a -> List b -> List b + // recursiveFunction mapper list acc = + // case list of + // [] -> + // acc + // x :: xs -> + // if cond then + // recursiveFunction mapper xs (mapper x :: acc) + // else + // recursiveFunction mapper xs <| (mapper x :: acc) + const initialCode = ` + var something$recursiveFunction = F3( + function (mapper, list, acc) { + recursiveFunction: + while (true) { + if (!list.b) { + return acc; + } else if (cond) { + var x = list.a; + var xs = list.b; + var $temp$mapper = mapper, + $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + mapper = $temp$mapper; + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } else { + return A3( + something$recursiveFunction, + mapper, + xs, + A2( + $elm$core$List$cons, + mapper(x), + acc)); + } + } + }); + `; + + // Corresponds to the following TCO-ed Elm code + // recursiveFunction : (a -> b) -> List a -> List b -> List b + // recursiveFunction mapper list acc = + // case list of + // [] -> + // acc + // x :: xs -> + // recursiveFunction mapper xs (mapper x :: acc) + const expectedOutputCode = ` + var something$recursiveFunction = F3( + function (mapper, list, acc) { + recursiveFunction: + while (true) { + if (!list.b) { + return acc; + } else if (cond) { + var x = list.a; + var xs = list.b; + var $temp$mapper = mapper, + $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + mapper = $temp$mapper; + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } else { + var x = list.a; + var xs = list.b; + var $temp$mapper = mapper, + $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + mapper = $temp$mapper; + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + test('should not change non-recursive functions', () => { const initialCode = ` var something$recursiveFunction = F3( From e5fc1146fe321f2866e3476a43c9edf6e2df36ea Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:19:33 +0100 Subject: [PATCH 037/229] Be more explicit in condition --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index d1d065b7..20835a7b 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -50,7 +50,7 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory Date: Wed, 29 Dec 2021 19:30:25 +0100 Subject: [PATCH 038/229] Re-use label --- src/transforms/tailCallRecursion.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 20835a7b..d58c5ddf 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -150,9 +150,17 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record return node; } - return ts.createBlock( - [ts.createLabel(label, ts.createWhile(ts.createTrue(), updatedBlock))] - ); + if (functionsToBeMadeRecursive[functionName] !== true) { + return body; + } + + if (!ts.isLabeledStatement(updatedBlock.statements[0])) { + return ts.createBlock( + [ts.createLabel(label, ts.createWhile(ts.createTrue(), updatedBlock))] + ); + } + + return updatedBlock; } function extractCallTo(functionName : string, node : ts.CallExpression) : Array | null { From a8519a3c50110bd1e27abf1a01901e64f137f9e0 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:32:10 +0100 Subject: [PATCH 039/229] Reset when exiting function to avoid issues with functions with the same name --- src/transforms/tailCallRecursion.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index d58c5ddf..db533318 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -53,6 +53,7 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory Date: Wed, 29 Dec 2021 19:33:50 +0100 Subject: [PATCH 040/229] Visit labeled statements --- src/transforms/tailCallRecursion.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index db533318..63ecd1a1 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -112,6 +112,10 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record return ts.visitEachChild(node, updateRecursiveCallVisitor, context); } + if (ts.isLabeledStatement(node)) { + return ts.visitEachChild(node, updateRecursiveCallVisitor, context); + } + if (ts.isIfStatement(node)) { return ts.updateIf( node, @@ -147,7 +151,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record ts.createContinue(label) ]; } - + console.log(node.kind) return node; } From 72de08aa6fbfcc5993342d6ed2e5d596f28e2e01 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:34:52 +0100 Subject: [PATCH 041/229] Visit while loops --- src/transforms/tailCallRecursion.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 63ecd1a1..99e885dd 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -116,6 +116,10 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record return ts.visitEachChild(node, updateRecursiveCallVisitor, context); } + if (ts.isWhileStatement(node)) { + return ts.visitEachChild(node, updateRecursiveCallVisitor, context); + } + if (ts.isIfStatement(node)) { return ts.updateIf( node, From 3ea78bf616e6f56e9a28e8bef387c326952b91e6 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:35:20 +0100 Subject: [PATCH 042/229] Merge conditions --- src/transforms/tailCallRecursion.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 99e885dd..b02daa8e 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -108,15 +108,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); function updateRecursiveCallVisitor(node: ts.Node): ts.VisitResult { - if (ts.isBlock(node)) { - return ts.visitEachChild(node, updateRecursiveCallVisitor, context); - } - - if (ts.isLabeledStatement(node)) { - return ts.visitEachChild(node, updateRecursiveCallVisitor, context); - } - - if (ts.isWhileStatement(node)) { + if (ts.isBlock(node) || ts.isLabeledStatement(node) || ts.isWhileStatement(node)) { return ts.visitEachChild(node, updateRecursiveCallVisitor, context); } From 0fd8a7f77e92e43f835881cd93a58a1f9ae1383c Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Wed, 29 Dec 2021 19:36:00 +0100 Subject: [PATCH 043/229] Remove TODO --- src/transforms/tailCallRecursion.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index b02daa8e..2a9b99f6 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -28,7 +28,6 @@ but this version doesn't (because of the additional `<|`): */ -// TODO Re-use the existing loop and goto label if there is already one // TODO Enable TCO for tail recursion modulo cons // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors From 81c5dea4c8738a0e354173b7fa47e4e69a4d33d8 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 00:41:40 +0100 Subject: [PATCH 044/229] Don't generate unnecessary variables --- src/transforms/tailCallRecursion.ts | 50 ++++++++++++++++++++--------- test/tailCallRecursion.test.ts | 4 +-- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 2a9b99f6..07059882 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -130,21 +130,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record } functionsToBeMadeRecursive[functionName] = true; - return [ - ts.createVariableDeclarationList( - parameterNames.map((name, index) => - ts.createVariableDeclaration( - `$temp$${name}`, - undefined, - newArguments[index] - ) - ) - ), - ...parameterNames.map(name => - ts.createAssignment(ts.createIdentifier(name), ts.createIdentifier(`$temp$${name}`)) - ), - ts.createContinue(label) - ]; + return createContinuation(label, parameterNames, newArguments); } console.log(node.kind) return node; @@ -183,4 +169,38 @@ function extractCallTo(functionName : string, node : ts.CallExpression) : Array< } return null; +} + +function createContinuation(label : string, parameterNames : Array, newArguments : Array) : Array { + let assignments : Array = []; + let reassignments : Array = []; + + parameterNames.forEach((name, index) => { + const correspondingArg : ts.Expression = newArguments[index]; + if (ts.isIdentifier(correspondingArg) + && name === correspondingArg.text + ) { + return; + } + const tempName = `$temp$${name}`; + assignments.push( + ts.createVariableDeclaration( + tempName, + undefined, + newArguments[index] + ) + ); + reassignments.push( + ts.createAssignment( + ts.createIdentifier(name), + ts.createIdentifier(tempName) + ) + ); + }); + + return [ + ts.createVariableDeclarationList(assignments), + ...reassignments, + ts.createContinue(label) + ]; } \ No newline at end of file diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 1e7ec1de..ba2d7ab2 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -81,13 +81,11 @@ test('it can turn a function that is tail-call recursive into a while loop', () } else { var x = list.a; var xs = list.b; - var $temp$mapper = mapper, - $temp$list = xs, + var $temp$list = xs, $temp$acc = A2( $elm$core$List$cons, mapper(x), acc); - mapper = $temp$mapper; list = $temp$list; acc = $temp$acc; continue recursiveFunction; From 3895f4aedc773c04809452f113041133af7644c5 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:00:31 +0100 Subject: [PATCH 045/229] Remove console.log --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 07059882..f4c3241d 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -132,7 +132,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record functionsToBeMadeRecursive[functionName] = true; return createContinuation(label, parameterNames, newArguments); } - console.log(node.kind) + return node; } From 5935a335ed25f5c8388a999a97d09ecb4eb02ad8 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:53:37 +0100 Subject: [PATCH 046/229] Remove unnecessary variables from test --- test/tailCallRecursion.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index ba2d7ab2..861178c4 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -180,13 +180,11 @@ test('should re-use the label and while loop if there already is one', () => { } else { var x = list.a; var xs = list.b; - var $temp$mapper = mapper, - $temp$list = xs, + var $temp$list = xs, $temp$acc = A2( $elm$core$List$cons, mapper(x), acc); - mapper = $temp$mapper; list = $temp$list; acc = $temp$acc; continue recursiveFunction; From 4ed258fb7c7c93824d01849c8d786f0de45e3e4f Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 00:15:41 +0100 Subject: [PATCH 047/229] Add failing test --- test/tailCallRecursion.test.ts | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 861178c4..1e0e3776 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -232,6 +232,60 @@ test('should not change non-recursive functions', () => { expect(actual).toBe(expected); }); +test('should optimize a function that cons (::) on the result of recursive calls', () => { + // Corresponds to the following Elm code + // map fn list = + // case list of + // [] -> [] + // x :: xs -> fn x :: map fn xs + const initialCode = ` + var $something F2( + function (fn, list) { + if (!list.b) { + return _List_Nil; + } else { + var x = list.a; + var xs = list.b; + return A2( + $elm$core$List$cons, + fn(x), + A2($something$map, fn, xs)); + } + }); + `; + + const expectedOutputCode = ` + var something$map = F2( + function (fn, list) { + var tmp = _List_Cons(undefined, _List_Nil); + var end = tmp; + map: + while (true) { + if (!list.b) { + end.b = acc; + return tmp.b; + } else { + var x = list.a; + var xs = list.b; + var $temp$fn = fn, + $temp$list = xs; + fn = $temp$fn; + list = $temp$list; + continue map; + } + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + export function transformCode( initialCode: string, expectedCode: string, From 11f07e9afc2e8633149aba29b8d2f2d289637648 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 00:16:15 +0100 Subject: [PATCH 048/229] only --- test/tailCallRecursion.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 1e0e3776..2eb7010b 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -232,7 +232,7 @@ test('should not change non-recursive functions', () => { expect(actual).toBe(expected); }); -test('should optimize a function that cons (::) on the result of recursive calls', () => { +test.only('should optimize a function that cons (::) on the result of recursive calls', () => { // Corresponds to the following Elm code // map fn list = // case list of From f7522412796bd714fb4844b2c02b07b07d5424a8 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 00:23:28 +0100 Subject: [PATCH 049/229] Fix test --- test/tailCallRecursion.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 2eb7010b..983c0bc2 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -262,13 +262,15 @@ test.only('should optimize a function that cons (::) on the result of recursive map: while (true) { if (!list.b) { - end.b = acc; return tmp.b; } else { var x = list.a; var xs = list.b; var $temp$fn = fn, $temp$list = xs; + var next = _List_Cons(fn(x), _List_Nil); + end.b = next; + end = next; fn = $temp$fn; list = $temp$list; continue map; From 2bf6aad97f1a20d5f41cf68e1e7ce76023a53da7 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:01:29 +0100 Subject: [PATCH 050/229] test --- test/tailCallRecursion.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 983c0bc2..fcd60347 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -266,12 +266,10 @@ test.only('should optimize a function that cons (::) on the result of recursive } else { var x = list.a; var xs = list.b; - var $temp$fn = fn, - $temp$list = xs; var next = _List_Cons(fn(x), _List_Nil); end.b = next; end = next; - fn = $temp$fn; + var $temp$list = xs; list = $temp$list; continue map; } From 26aaff38b0ca0556fea6fabf5f6fc3b356668d9d Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:42:40 +0100 Subject: [PATCH 051/229] Add recursion type --- src/transforms/tailCallRecursion.ts | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f4c3241d..f72c07e4 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -101,6 +101,49 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { return null; } +enum RecursionType { + NotRecursive = 0, + PlainRecursion = 1, +}; + +function determineRecursionType(functionName : string, body : ts.Node) : RecursionType { + let recursionType : RecursionType = RecursionType.NotRecursive; + let nodesToVisit : Array = [body]; + let node : ts.Node | undefined; + + loop: while (recursionType <= 1 && (node = nodesToVisit.shift())) { + if (ts.isBlock(node)) { + nodesToVisit = [...node.statements, ...nodesToVisit]; + continue loop; + } + + if (ts.isLabeledStatement(node)) { + recursionType = RecursionType.PlainRecursion; + continue loop; + } + + if (ts.isWhileStatement(node)) { + recursionType = RecursionType.PlainRecursion; + continue loop; + } + + if (ts.isIfStatement(node)) { + [node.thenStatement, node.elseStatement, ...nodesToVisit] + continue loop; + } + if (ts.isReturnStatement(node) + && node.expression + && ts.isCallExpression(node.expression) + && extractCallTo(functionName, node.expression) !== null + ) { + recursionType = RecursionType.PlainRecursion; + continue loop; + } + } + + return recursionType; +} + function updateFunctionBody(functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; From ac455a727ff0e7a15433a9de871a2e690a79b54b Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:44:16 +0100 Subject: [PATCH 052/229] Early return --- src/transforms/tailCallRecursion.ts | 66 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f72c07e4..19df6e5b 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -44,39 +44,41 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory = fn.parameters.map(param => { - return ts.isIdentifier(param.name) ? param.name.text : ''; - }); - const newBody = updateFunctionBody(functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); - if (functionsToBeMadeRecursive[node.name.text] !== true) { - return node; - } - functionsToBeMadeRecursive[node.name.text] = false; - const newFn = ts.createFunctionExpression( - fn.modifiers, - undefined, - fn.name, - undefined, - fn.parameters, - undefined, - newBody - ); - - const initializer = ts.updateCall( - node.initializer, - node.initializer.expression, - undefined, - [newFn] - ); - - return ts.updateVariableDeclaration( - node, - node.name, - undefined, - initializer - ); + if (!fn) { + return ts.visitEachChild(node, visitor, context); } + + const parameterNames : Array = fn.parameters.map(param => { + return ts.isIdentifier(param.name) ? param.name.text : ''; + }); + const newBody = updateFunctionBody(functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); + if (functionsToBeMadeRecursive[node.name.text] !== true) { + return node; + } + functionsToBeMadeRecursive[node.name.text] = false; + const newFn = ts.createFunctionExpression( + fn.modifiers, + undefined, + fn.name, + undefined, + fn.parameters, + undefined, + newBody + ); + + const initializer = ts.updateCall( + node.initializer, + node.initializer.expression, + undefined, + [newFn] + ); + + return ts.updateVariableDeclaration( + node, + node.name, + undefined, + initializer + ); } return ts.visitEachChild(node, visitor, context); }; From 2e0e072584d4e19a53b40d66ce088c2cb459f82d Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:45:55 +0100 Subject: [PATCH 053/229] only --- test/tailCallRecursion.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index fcd60347..b3154eb0 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -232,7 +232,7 @@ test('should not change non-recursive functions', () => { expect(actual).toBe(expected); }); -test.only('should optimize a function that cons (::) on the result of recursive calls', () => { +test('should optimize a function that cons (::) on the result of recursive calls', () => { // Corresponds to the following Elm code // map fn list = // case list of From 982fa9f0f7e89f81bcc117637fc4e93001f2ab2a Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:47:10 +0100 Subject: [PATCH 054/229] Use the recursion type --- src/transforms/tailCallRecursion.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 19df6e5b..58582e42 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -47,15 +47,16 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory = fn.parameters.map(param => { return ts.isIdentifier(param.name) ? param.name.text : ''; }); const newBody = updateFunctionBody(functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); - if (functionsToBeMadeRecursive[node.name.text] !== true) { - return node; - } - functionsToBeMadeRecursive[node.name.text] = false; + const newFn = ts.createFunctionExpression( fn.modifiers, undefined, @@ -133,6 +134,7 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi [node.thenStatement, node.elseStatement, ...nodesToVisit] continue loop; } + if (ts.isReturnStatement(node) && node.expression && ts.isCallExpression(node.expression) From d4d9f8273895d26a3c5f6dd7afed68a52ad3eac2 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:51:23 +0100 Subject: [PATCH 055/229] Visit if correctly --- src/transforms/tailCallRecursion.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 58582e42..e97af0ac 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -131,7 +131,10 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi } if (ts.isIfStatement(node)) { - [node.thenStatement, node.elseStatement, ...nodesToVisit] + if (node.elseStatement) { + nodesToVisit.unshift(node.elseStatement); + } + nodesToVisit.unshift(node.thenStatement); continue loop; } From 7a1f17b6fc4098a5e821d45367dd777f07b65092 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:52:31 +0100 Subject: [PATCH 056/229] Visit other nodes as well --- src/transforms/tailCallRecursion.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index e97af0ac..f4abb46c 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -122,11 +122,13 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi if (ts.isLabeledStatement(node)) { recursionType = RecursionType.PlainRecursion; + nodesToVisit.unshift(node.statement); continue loop; } if (ts.isWhileStatement(node)) { recursionType = RecursionType.PlainRecursion; + nodesToVisit.unshift(node.expression); continue loop; } From b46334e3080b2b950d0d97238544373952daa0ee Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 01:57:38 +0100 Subject: [PATCH 057/229] Add extractRecursionKindFromReturn --- src/transforms/tailCallRecursion.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f4abb46c..1aaf7230 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -201,6 +201,33 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record return updatedBlock; } +function extractRecursionKindFromReturn(functionName : string, node : ts.CallExpression) : RecursionType { + if (!ts.isIdentifier(node.expression)) { + return RecursionType.NotRecursive; + } + + // Is "fn(...)" + if (node.expression.text === functionName) { + return RecursionType.PlainRecursion; + } + + // Is "AX(fn, ...)" + const firstArg = node.arguments[0]; + if (!node.expression.text.startsWith("A") + || !ts.isIdentifier(firstArg) + ) { + return RecursionType.NotRecursive; + } + + + + if (firstArg.text === functionName) { + return RecursionType.PlainRecursion; + } + + return RecursionType.NotRecursive; +} + function extractCallTo(functionName : string, node : ts.CallExpression) : Array | null { if (!ts.isIdentifier(node.expression)) { return null; From e72029eb81a409dfb82c8475e10d3739083de626 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 02:06:30 +0100 Subject: [PATCH 058/229] Consider :: recursion --- src/transforms/tailCallRecursion.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 1aaf7230..51568d6c 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -212,19 +212,23 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp } // Is "AX(fn, ...)" - const firstArg = node.arguments[0]; + const [firstArg, , thirdArg] = node.arguments; if (!node.expression.text.startsWith("A") || !ts.isIdentifier(firstArg) ) { return RecursionType.NotRecursive; } - - if (firstArg.text === functionName) { return RecursionType.PlainRecursion; } + if (firstArg.text === "$elm$core$List$cons" && ts.isCallExpression(thirdArg)) { + if (extractRecursionKindFromReturn(functionName, thirdArg)) { + return RecursionType.PlainRecursion; + } + } + return RecursionType.NotRecursive; } From 9d156579d3c60812ad11c4c287130d6922158aa8 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 02:06:46 +0100 Subject: [PATCH 059/229] Add ConsRecursion --- src/transforms/tailCallRecursion.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 51568d6c..78dbd7fa 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -107,6 +107,7 @@ function isFCall(node: ts.CallExpression): ts.FunctionExpression | null { enum RecursionType { NotRecursive = 0, PlainRecursion = 1, + ConsRecursion = 2, }; function determineRecursionType(functionName : string, body : ts.Node) : RecursionType { From 092a38e0b7a2e2ed6f0e4b9c9153b42e7722953f Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 02:07:08 +0100 Subject: [PATCH 060/229] Consider :: as ConsRecursion --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 78dbd7fa..95c316e0 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -226,7 +226,7 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp if (firstArg.text === "$elm$core$List$cons" && ts.isCallExpression(thirdArg)) { if (extractRecursionKindFromReturn(functionName, thirdArg)) { - return RecursionType.PlainRecursion; + return RecursionType.ConsRecursion; } } From b4fffe801c5f5904c63650df3aaabf6acd2f80ab Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 02:08:53 +0100 Subject: [PATCH 061/229] Add TODOs --- src/transforms/tailCallRecursion.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 95c316e0..3abf6bc0 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -224,6 +224,7 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp return RecursionType.PlainRecursion; } + // TODO Add explanation if (firstArg.text === "$elm$core$List$cons" && ts.isCallExpression(thirdArg)) { if (extractRecursionKindFromReturn(functionName, thirdArg)) { return RecursionType.ConsRecursion; @@ -233,6 +234,9 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp return RecursionType.NotRecursive; } +// TODO Change extractCallTo to return a custom type that contains the kind of recursion +// plus the necessary data. And then re-use this function instead of extractRecursionKindFromReturn + function extractCallTo(functionName : string, node : ts.CallExpression) : Array | null { if (!ts.isIdentifier(node.expression)) { return null; From 73957e2a1367a1e639494bee8ce8122daa74fc0e Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 09:31:00 +0100 Subject: [PATCH 062/229] Add Recursion --- src/transforms/tailCallRecursion.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 3abf6bc0..7243bded 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -110,6 +110,11 @@ enum RecursionType { ConsRecursion = 2, }; +type Recursion + = { kind: RecursionType.NotRecursive } + | { kind: RecursionType.PlainRecursion, arguments : Array } + | { kind: RecursionType.ConsRecursion, element : ts.Expression, arguments : Array } + function determineRecursionType(functionName : string, body : ts.Node) : RecursionType { let recursionType : RecursionType = RecursionType.NotRecursive; let nodesToVisit : Array = [body]; From 83e0ce0672bee5950296e393aee608492cc6c886 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 09:37:45 +0100 Subject: [PATCH 063/229] Change the return type --- src/transforms/tailCallRecursion.ts | 34 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 7243bded..3c0edb4b 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -113,6 +113,7 @@ enum RecursionType { type Recursion = { kind: RecursionType.NotRecursive } | { kind: RecursionType.PlainRecursion, arguments : Array } + // Could hold a Recursion as data | { kind: RecursionType.ConsRecursion, element : ts.Expression, arguments : Array } function determineRecursionType(functionName : string, body : ts.Node) : RecursionType { @@ -207,36 +208,49 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record return updatedBlock; } -function extractRecursionKindFromReturn(functionName : string, node : ts.CallExpression) : RecursionType { +function extractRecursionKindFromReturn(functionName : string, node : ts.CallExpression) : Recursion { if (!ts.isIdentifier(node.expression)) { - return RecursionType.NotRecursive; + return { kind: RecursionType.NotRecursive }; } // Is "fn(...)" if (node.expression.text === functionName) { - return RecursionType.PlainRecursion; + return { + kind: RecursionType.PlainRecursion, + arguments: [...node.arguments] + }; } // Is "AX(fn, ...)" - const [firstArg, , thirdArg] = node.arguments; + const [firstArg, secondArg, thirdArg] = node.arguments; if (!node.expression.text.startsWith("A") || !ts.isIdentifier(firstArg) ) { - return RecursionType.NotRecursive; + return { kind: RecursionType.NotRecursive }; } if (firstArg.text === functionName) { - return RecursionType.PlainRecursion; + return { + kind: RecursionType.PlainRecursion, + arguments: node.arguments.slice(1) + }; } - // TODO Add explanation + // Elm: Is x :: + // JS: Is A2($elm$core$List$cons, x, ) if (firstArg.text === "$elm$core$List$cons" && ts.isCallExpression(thirdArg)) { - if (extractRecursionKindFromReturn(functionName, thirdArg)) { - return RecursionType.ConsRecursion; + const thirdArgExtract = extractRecursionKindFromReturn(functionName, thirdArg); + if (thirdArgExtract.kind === RecursionType.PlainRecursion) { + // TODO Support multiple conses + return { + kind: RecursionType.ConsRecursion, + element: secondArg, + arguments: thirdArgExtract.arguments + }; } } - return RecursionType.NotRecursive; + return { kind: RecursionType.NotRecursive }; } // TODO Change extractCallTo to return a custom type that contains the kind of recursion From 1964eebe794b0a44b01bca42a3758b3bc43f2a00 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:04:18 +0100 Subject: [PATCH 064/229] Re-use extractRecursionKindFromReturn --- src/transforms/tailCallRecursion.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 3c0edb4b..3dd2b975 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -150,9 +150,11 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi if (ts.isReturnStatement(node) && node.expression && ts.isCallExpression(node.expression) - && extractCallTo(functionName, node.expression) !== null ) { - recursionType = RecursionType.PlainRecursion; + recursionType = Math.max( + extractRecursionKindFromReturn(functionName, node.expression).kind, + recursionType + ); continue loop; } } From 8246fb4f26331914df2a2a3e18a7b7cd190edfc0 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:12:05 +0100 Subject: [PATCH 065/229] Use extractRecursionKindFromReturn --- src/transforms/tailCallRecursion.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 3dd2b975..1f39f7a3 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -185,13 +185,18 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record && node.expression && ts.isCallExpression(node.expression) ) { - const newArguments = extractCallTo(functionName, node.expression); - if (!newArguments) { - return node; - } + const extract = extractRecursionKindFromReturn(functionName, node.expression); + functionsToBeMadeRecursive[functionName] = functionsToBeMadeRecursive[functionName] || extract.kind !== RecursionType.NotRecursive; + + switch (extract.kind) { + case RecursionType.NotRecursive: { + return node; + } - functionsToBeMadeRecursive[functionName] = true; - return createContinuation(label, parameterNames, newArguments); + case RecursionType.PlainRecursion: { + return createContinuation(label, parameterNames, extract.arguments); + } + } } return node; From 5197198bae2e896cc4d561b7b76e965c0def3b55 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:12:20 +0100 Subject: [PATCH 066/229] Remove extractCallTo --- src/transforms/tailCallRecursion.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 1f39f7a3..2548d08a 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -260,31 +260,6 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp return { kind: RecursionType.NotRecursive }; } -// TODO Change extractCallTo to return a custom type that contains the kind of recursion -// plus the necessary data. And then re-use this function instead of extractRecursionKindFromReturn - -function extractCallTo(functionName : string, node : ts.CallExpression) : Array | null { - if (!ts.isIdentifier(node.expression)) { - return null; - } - - // Is "fn(...)" - if (node.expression.text === functionName) { - return [...node.arguments]; - } - - // Is "AX(fn, ...)" - const firstArg = node.arguments[0]; - if (node.expression.text.startsWith("A") - && ts.isIdentifier(firstArg) - && firstArg.text === functionName - ) { - return node.arguments.slice(1); - } - - return null; -} - function createContinuation(label : string, parameterNames : Array, newArguments : Array) : Array { let assignments : Array = []; let reassignments : Array = []; From 4fd84bb945a2f065f9b7e625764e85accaffc554 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:14:42 +0100 Subject: [PATCH 067/229] Fix test --- test/tailCallRecursion.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index b3154eb0..47dfa208 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -239,7 +239,7 @@ test('should optimize a function that cons (::) on the result of recursive calls // [] -> [] // x :: xs -> fn x :: map fn xs const initialCode = ` - var $something F2( + var $something$map = F2( function (fn, list) { if (!list.b) { return _List_Nil; @@ -255,7 +255,7 @@ test('should optimize a function that cons (::) on the result of recursive calls `; const expectedOutputCode = ` - var something$map = F2( + var $something$map = F2( function (fn, list) { var tmp = _List_Cons(undefined, _List_Nil); var end = tmp; From 1e68381ae5978752c96f6678c63859fb1e659b38 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:17:20 +0100 Subject: [PATCH 068/229] Support cons recursion start --- src/transforms/tailCallRecursion.ts | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 2548d08a..7ad8eb39 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -196,6 +196,10 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record case RecursionType.PlainRecursion: { return createContinuation(label, parameterNames, extract.arguments); } + + case RecursionType.ConsRecursion: { + return createConsContinuation(label, parameterNames, extract.element, extract.arguments); + } } } @@ -287,6 +291,41 @@ function createContinuation(label : string, parameterNames : Array, newA ); }); + return [ + ts.createVariableDeclarationList(assignments), + ...reassignments, + ts.createContinue(label) + ]; +} + +function createConsContinuation(label : string, parameterNames : Array, element : ts.Expression, newArguments : Array) : Array { + console.log(element) + let assignments : Array = []; + let reassignments : Array = []; + + parameterNames.forEach((name, index) => { + const correspondingArg : ts.Expression = newArguments[index]; + if (ts.isIdentifier(correspondingArg) + && name === correspondingArg.text + ) { + return; + } + const tempName = `$temp$${name}`; + assignments.push( + ts.createVariableDeclaration( + tempName, + undefined, + newArguments[index] + ) + ); + reassignments.push( + ts.createAssignment( + ts.createIdentifier(name), + ts.createIdentifier(tempName) + ) + ); + }); + return [ ts.createVariableDeclarationList(assignments), ...reassignments, From d46de280fe333b6ab265af94ad53358226f9a248 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:46:45 +0100 Subject: [PATCH 069/229] Make functionsToBeMadeRecursive contain a RecursionType --- src/transforms/tailCallRecursion.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 7ad8eb39..50d3234c 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -36,7 +36,7 @@ type Context = any; export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : Context) => { return (sourceFile) => { - const functionsToBeMadeRecursive : Record = {}; + const functionsToBeMadeRecursive : Record = {}; const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) @@ -162,7 +162,7 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi return recursionType; } -function updateFunctionBody(functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { +function updateFunctionBody(functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); @@ -186,7 +186,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record && ts.isCallExpression(node.expression) ) { const extract = extractRecursionKindFromReturn(functionName, node.expression); - functionsToBeMadeRecursive[functionName] = functionsToBeMadeRecursive[functionName] || extract.kind !== RecursionType.NotRecursive; + functionsToBeMadeRecursive[functionName] = Math.max(functionsToBeMadeRecursive[functionName], extract.kind); switch (extract.kind) { case RecursionType.NotRecursive: { @@ -206,7 +206,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record return node; } - if (functionsToBeMadeRecursive[functionName] !== true) { + if (functionsToBeMadeRecursive[functionName]) { return body; } From 70fd9a6de331b8279e015e5da873c83d8c48002c Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:49:08 +0100 Subject: [PATCH 070/229] Add TODO --- src/transforms/tailCallRecursion.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 50d3234c..e38e71de 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -31,6 +31,7 @@ but this version doesn't (because of the additional `<|`): // TODO Enable TCO for tail recursion modulo cons // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors +// TODO Enable TCO for let declarations type Context = any; From e15b915d99db1ec94e7d044521b194f2d4a282cc Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 10:55:12 +0100 Subject: [PATCH 071/229] Change logic structure --- src/transforms/tailCallRecursion.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index e38e71de..cb25fbb3 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -207,14 +207,31 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record Date: Thu, 30 Dec 2021 11:10:43 +0100 Subject: [PATCH 072/229] Add cons-specific declarations --- src/transforms/tailCallRecursion.ts | 38 +++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index cb25fbb3..ec599bbd 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -163,6 +163,34 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi return recursionType; } +const consDeclarations = + [ + ts.createVariableStatement( + undefined, + [ts.createVariableDeclaration( + "tmp", + undefined, + ts.createCall( + ts.createIdentifier("_List_Cons"), + undefined, + [ + ts.createIdentifier("undefined"), + ts.createIdentifier("_List_Nil") + ] + ) + )] + ), + ts.createVariableStatement( + undefined, + [ ts.createVariableDeclaration( + "end", + undefined, + ts.createIdentifier("tmp") + ) + ] + ) + ]; + function updateFunctionBody(functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; @@ -227,11 +255,17 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record Date: Thu, 30 Dec 2021 11:16:06 +0100 Subject: [PATCH 073/229] Avoid NaN --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index ec599bbd..ecb13aa0 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -215,7 +215,7 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record Date: Thu, 30 Dec 2021 11:19:49 +0100 Subject: [PATCH 074/229] Use recursion type --- src/transforms/tailCallRecursion.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index ecb13aa0..0de5156a 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -56,7 +56,7 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory = fn.parameters.map(param => { return ts.isIdentifier(param.name) ? param.name.text : ''; }); - const newBody = updateFunctionBody(functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); + const newBody = updateFunctionBody(recursionType, functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); const newFn = ts.createFunctionExpression( fn.modifiers, @@ -191,7 +191,7 @@ const consDeclarations = ) ]; -function updateFunctionBody(functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { +function updateFunctionBody(recursionType : RecursionType, functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); @@ -235,14 +235,11 @@ function updateFunctionBody(functionsToBeMadeRecursive : Record Date: Thu, 30 Dec 2021 11:20:42 +0100 Subject: [PATCH 075/229] Remove functionsToBeMadeRecursive --- src/transforms/tailCallRecursion.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 0de5156a..31f37d2f 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -37,8 +37,6 @@ type Context = any; export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : Context) => { return (sourceFile) => { - const functionsToBeMadeRecursive : Record = {}; - const visitor = (node: ts.Node): ts.VisitResult => { if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) @@ -56,7 +54,7 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory = fn.parameters.map(param => { return ts.isIdentifier(param.name) ? param.name.text : ''; }); - const newBody = updateFunctionBody(recursionType, functionsToBeMadeRecursive, node.name.text, parameterNames, fn.body, context); + const newBody = updateFunctionBody(recursionType, node.name.text, parameterNames, fn.body, context); const newFn = ts.createFunctionExpression( fn.modifiers, @@ -191,7 +189,7 @@ const consDeclarations = ) ]; -function updateFunctionBody(recursionType : RecursionType, functionsToBeMadeRecursive : Record, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { +function updateFunctionBody(recursionType : RecursionType, functionName : string, parameterNames : Array, body : ts.Block, context : Context) : ts.Block { const labelSplits = functionName.split("$"); const label = labelSplits[labelSplits.length - 1] || functionName; const updatedBlock = ts.visitEachChild(body, updateRecursiveCallVisitor, context); @@ -215,7 +213,6 @@ function updateFunctionBody(recursionType : RecursionType, functionsToBeMadeRecu && ts.isCallExpression(node.expression) ) { const extract = extractRecursionKindFromReturn(functionName, node.expression); - functionsToBeMadeRecursive[functionName] = Math.max(functionsToBeMadeRecursive[functionName] || RecursionType.NotRecursive, extract.kind); switch (extract.kind) { case RecursionType.NotRecursive: { From d52828b968915a6e7f458fd09259a04e872446c7 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:01:57 +0100 Subject: [PATCH 076/229] Generate tail value assignment --- src/transforms/tailCallRecursion.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 31f37d2f..7fdaa320 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -345,7 +345,6 @@ function createContinuation(label : string, parameterNames : Array, newA } function createConsContinuation(label : string, parameterNames : Array, element : ts.Expression, newArguments : Array) : Array { - console.log(element) let assignments : Array = []; let reassignments : Array = []; @@ -373,6 +372,24 @@ function createConsContinuation(label : string, parameterNames : Array, }); return [ + ts.createExpressionStatement( + ts.createAssignment( + ts.createPropertyAccess( + ts.createIdentifier("end"), + "b" + ), + element + ) + ), + ts.createExpressionStatement( + ts.createAssignment( + ts.createIdentifier("end"), + ts.createPropertyAccess( + ts.createIdentifier("end"), + "b" + ) + ) + ), ts.createVariableDeclarationList(assignments), ...reassignments, ts.createContinue(label) From 357abdb2c56084926abd103404b2bf6b6cceba43 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:02:26 +0100 Subject: [PATCH 077/229] Update test --- test/tailCallRecursion.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 47dfa208..a26904f3 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -266,9 +266,8 @@ test('should optimize a function that cons (::) on the result of recursive calls } else { var x = list.a; var xs = list.b; - var next = _List_Cons(fn(x), _List_Nil); - end.b = next; - end = next; + end.b = _List_Cons(fn(x), _List_Nil); + end = end.b; var $temp$list = xs; list = $temp$list; continue map; From 50e13223961e955298cd8fccc7c63d8da6999f7f Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:05:35 +0100 Subject: [PATCH 078/229] Use cons --- src/transforms/tailCallRecursion.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 7fdaa320..9b790028 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -378,7 +378,14 @@ function createConsContinuation(label : string, parameterNames : Array, ts.createIdentifier("end"), "b" ), - element + ts.createCall( + ts.createIdentifier("_List_Cons"), + undefined, + [ + element, + ts.createIdentifier("_List_Nil") + ] + ) ) ), ts.createExpressionStatement( From 3098572f21fa04851bfad762e3d2a5e916723884 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:11:16 +0100 Subject: [PATCH 079/229] Extract function --- src/transforms/tailCallRecursion.ts | 36 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 9b790028..34a8dcb7 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -372,22 +372,7 @@ function createConsContinuation(label : string, parameterNames : Array, }); return [ - ts.createExpressionStatement( - ts.createAssignment( - ts.createPropertyAccess( - ts.createIdentifier("end"), - "b" - ), - ts.createCall( - ts.createIdentifier("_List_Cons"), - undefined, - [ - element, - ts.createIdentifier("_List_Nil") - ] - ) - ) - ), + addToEnd(element), ts.createExpressionStatement( ts.createAssignment( ts.createIdentifier("end"), @@ -401,4 +386,23 @@ function createConsContinuation(label : string, parameterNames : Array, ...reassignments, ts.createContinue(label) ]; +} + +function addToEnd(element : ts.Expression) : ts.Statement { + return ts.createExpressionStatement( + ts.createAssignment( + ts.createPropertyAccess( + ts.createIdentifier("end"), + "b" + ), + ts.createCall( + ts.createIdentifier("_List_Cons"), + undefined, + [ + element, + ts.createIdentifier("_List_Nil") + ] + ) + ) + ); } \ No newline at end of file From a3ab52ce3088acfb049452fb9343c7f12d45c63b Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:12:52 +0100 Subject: [PATCH 080/229] Add ; to the assignments --- src/transforms/tailCallRecursion.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 34a8dcb7..eea39ead 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -346,7 +346,7 @@ function createContinuation(label : string, parameterNames : Array, newA function createConsContinuation(label : string, parameterNames : Array, element : ts.Expression, newArguments : Array) : Array { let assignments : Array = []; - let reassignments : Array = []; + let reassignments : Array = []; parameterNames.forEach((name, index) => { const correspondingArg : ts.Expression = newArguments[index]; @@ -364,9 +364,11 @@ function createConsContinuation(label : string, parameterNames : Array, ) ); reassignments.push( - ts.createAssignment( - ts.createIdentifier(name), - ts.createIdentifier(tempName) + ts.createExpressionStatement( + ts.createAssignment( + ts.createIdentifier(name), + ts.createIdentifier(tempName) + ) ) ); }); From 3d99f7c034a947799eb51b0f45eb0ada31c43744 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:16:14 +0100 Subject: [PATCH 081/229] Move conditions --- src/transforms/tailCallRecursion.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index eea39ead..7d30c78f 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -210,21 +210,22 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string if (ts.isReturnStatement(node) && node.expression - && ts.isCallExpression(node.expression) ) { - const extract = extractRecursionKindFromReturn(functionName, node.expression); + if (ts.isCallExpression(node.expression)) { + const extract = extractRecursionKindFromReturn(functionName, node.expression); - switch (extract.kind) { - case RecursionType.NotRecursive: { - return node; - } + switch (extract.kind) { + case RecursionType.NotRecursive: { + return node; + } - case RecursionType.PlainRecursion: { - return createContinuation(label, parameterNames, extract.arguments); - } + case RecursionType.PlainRecursion: { + return createContinuation(label, parameterNames, extract.arguments); + } - case RecursionType.ConsRecursion: { - return createConsContinuation(label, parameterNames, extract.element, extract.arguments); + case RecursionType.ConsRecursion: { + return createConsContinuation(label, parameterNames, extract.element, extract.arguments); + } } } } From 1d388a1e2e04e46715a23a0ee7768ef8c6a5dfac Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:19:08 +0100 Subject: [PATCH 082/229] Return the head --- src/transforms/tailCallRecursion.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 7d30c78f..8f7cd77f 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -228,6 +228,17 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string } } } + + if (recursionType === RecursionType.ConsRecursion) { + return [ + ts.createReturn( + ts.createPropertyAccess( + ts.createIdentifier("tmp"), + "b" + ) + ) + ]; + } } return node; From fb87927346ad8175b2d556cf64159c090bd66ac7 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:22:11 +0100 Subject: [PATCH 083/229] Set end --- src/transforms/tailCallRecursion.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 8f7cd77f..fc40064b 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -231,6 +231,15 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string if (recursionType === RecursionType.ConsRecursion) { return [ + ts.createExpressionStatement( + ts.createAssignment( + ts.createPropertyAccess( + ts.createIdentifier("end"), + "b" + ), + node.expression + ) + ), ts.createReturn( ts.createPropertyAccess( ts.createIdentifier("tmp"), From 2c6f0842330aa79b890720cc1ee247c51dd0e712 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:23:55 +0100 Subject: [PATCH 084/229] Don't set the end if it's [] --- src/transforms/tailCallRecursion.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index fc40064b..f8dcd539 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -230,6 +230,17 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string } if (recursionType === RecursionType.ConsRecursion) { + const returnStatement = ts.createReturn( + ts.createPropertyAccess( + ts.createIdentifier("tmp"), + "b" + ) + ); + + if (ts.isIdentifier(node.expression) && node.expression.text === "_List_Nil") { + return returnStatement; + } + return [ ts.createExpressionStatement( ts.createAssignment( @@ -240,12 +251,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string node.expression ) ), - ts.createReturn( - ts.createPropertyAccess( - ts.createIdentifier("tmp"), - "b" - ) - ) + returnStatement ]; } } From 56ba99b5ece2df14d32963ea7fd1837eb75d2430 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 16:34:39 +0100 Subject: [PATCH 085/229] Remove TODO --- src/transforms/tailCallRecursion.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f8dcd539..e37adb43 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -28,7 +28,6 @@ but this version doesn't (because of the additional `<|`): */ -// TODO Enable TCO for tail recursion modulo cons // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors // TODO Enable TCO for let declarations From d2635bce864a4ed7291b90da9ac1d4ca5d964f60 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 23:37:23 +0100 Subject: [PATCH 086/229] Add test for filter --- test/tailCallRecursion.test.ts | 78 +++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index a26904f3..c2b4b9bf 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -232,7 +232,7 @@ test('should not change non-recursive functions', () => { expect(actual).toBe(expected); }); -test('should optimize a function that cons (::) on the result of recursive calls', () => { +test('should optimize a function that cons (::) on the result of recursive calls (List.map)', () => { // Corresponds to the following Elm code // map fn list = // case list of @@ -285,6 +285,82 @@ test('should optimize a function that cons (::) on the result of recursive calls expect(actual).toBe(expected); }); +test('should optimize a function that cons (::) on the result of recursive calls (List.filter)', () => { + // Corresponds to the following Elm code + // filter : (a -> Bool) -> List a -> List a + // filter predicate list = + // case list of + // [] -> + // [] + // + // x :: xs -> + // if predicate x then + // x :: filter predicate xs + // + // else + // filter predicate xs + const initialCode = ` + var $something$filter = F2( + function (predicate, list) { + filter: + while (true) { + if (!list.b) { + return _List_Nil; + } else { + var x = list.a; + var xs = list.b; + if (predicate(x)) { + return A2( + $elm$core$List$cons, + x, + A2($something$filter, predicate, xs)); + } else { + var $temp$list = xs; + list = $temp$list; + continue filter; + } + } + } + }); + `; + + const expectedOutputCode = ` + var $something$filter = F2( + function (predicate, list) { + var tmp = _List_Cons(undefined, _List_Nil); + var end = tmp; + filter: + while (true) { + if (!list.b) { + return tmp.b; + } else { + var x = list.a; + var xs = list.b; + if (predicate(x)) { + end.b = _List_Cons(x, _List_Nil); + end = end.b; + var $temp$list = xs; + list = $temp$list; + continue filter; + } else { + var $temp$list = xs; + list = $temp$list; + continue filter; + } + } + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + export function transformCode( initialCode: string, expectedCode: string, From f579a31c754f1ff38450cb941364837a3e147f92 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Thu, 30 Dec 2021 23:51:53 +0100 Subject: [PATCH 087/229] Visit while body --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index e37adb43..bd9d403c 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -133,7 +133,7 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi if (ts.isWhileStatement(node)) { recursionType = RecursionType.PlainRecursion; - nodesToVisit.unshift(node.expression); + nodesToVisit.unshift(node.statement); continue loop; } From 3df6d8744adeb0a12bce31d10d3b92c759a4d9eb Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Fri, 31 Dec 2021 10:02:04 +0100 Subject: [PATCH 088/229] Remove additional block --- src/transforms/tailCallRecursion.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index bd9d403c..01b433ae 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -281,9 +281,10 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string ); } - return ts.createBlock( + return ts.updateBlock( + updatedBlock, [ ...consDeclarations - , updatedBlock + , ...updatedBlock.statements ] ); } From bbc31bbe3866f75080e0e992948bcac809a30612 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Fri, 31 Dec 2021 10:41:31 +0100 Subject: [PATCH 089/229] Support multiple conses --- src/transforms/tailCallRecursion.ts | 104 +++++++++++++++++++--------- test/tailCallRecursion.test.ts | 55 +++++++++++++++ 2 files changed, 128 insertions(+), 31 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 01b433ae..2d50c4e4 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -37,46 +37,31 @@ type Context = any; export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : Context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { + // Is `var x = FX(function(...) {})` if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer - && ts.isCallExpression(node.initializer)) { - const fn = isFCall(node.initializer); - if (!fn) { + ) { + const foundFunction = findFunction(node.initializer); + if (!foundFunction) { return ts.visitEachChild(node, visitor, context); } - const recursionType : RecursionType = determineRecursionType(node.name.text, fn.body); + + const recursionType : RecursionType = determineRecursionType(node.name.text, foundFunction.fn.body); if (recursionType === RecursionType.NotRecursive) { return ts.visitEachChild(node, visitor, context); } - const parameterNames : Array = fn.parameters.map(param => { + const parameterNames : Array = foundFunction.fn.parameters.map(param => { return ts.isIdentifier(param.name) ? param.name.text : ''; }); - const newBody = updateFunctionBody(recursionType, node.name.text, parameterNames, fn.body, context); - - const newFn = ts.createFunctionExpression( - fn.modifiers, - undefined, - fn.name, - undefined, - fn.parameters, - undefined, - newBody - ); - - const initializer = ts.updateCall( - node.initializer, - node.initializer.expression, - undefined, - [newFn] - ); + const newBody = updateFunctionBody(recursionType, node.name.text, parameterNames, foundFunction.fn.body, context); return ts.updateVariableDeclaration( node, node.name, undefined, - initializer + foundFunction.update(newBody) ); } return ts.visitEachChild(node, visitor, context); @@ -86,7 +71,60 @@ export const createTailCallRecursionTransformer : ts.TransformerFactory { + const newFn = ts.createFunctionExpression( + modifiers, + undefined, + name, + undefined, + parameters, + undefined, + body + ); + + return ts.updateCall( + node, + node.expression, + undefined, + [newFn] + ); + } + } + } + + // Single-argument function not wrapped in FX + if (ts.isFunctionExpression(node)) { + return { + fn: node, + update: (body : ts.Block) => { + return ts.createFunctionExpression( + node.modifiers, + undefined, + node.name, + undefined, + node.parameters, + undefined, + body + ); + } + } + } + + return null; +} + +function extractFCall(node: ts.CallExpression): ts.FunctionExpression | null { if (ts.isIdentifier(node.expression) && node.expression.text.startsWith('F') && node.arguments.length > 0 @@ -112,7 +150,7 @@ type Recursion = { kind: RecursionType.NotRecursive } | { kind: RecursionType.PlainRecursion, arguments : Array } // Could hold a Recursion as data - | { kind: RecursionType.ConsRecursion, element : ts.Expression, arguments : Array } + | { kind: RecursionType.ConsRecursion, elements : ts.Expression[], arguments : Array } function determineRecursionType(functionName : string, body : ts.Node) : RecursionType { let recursionType : RecursionType = RecursionType.NotRecursive; @@ -223,7 +261,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string } case RecursionType.ConsRecursion: { - return createConsContinuation(label, parameterNames, extract.element, extract.arguments); + return createConsContinuation(label, parameterNames, extract.elements, extract.arguments); } } } @@ -325,13 +363,17 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp if (firstArg.text === "$elm$core$List$cons" && ts.isCallExpression(thirdArg)) { const thirdArgExtract = extractRecursionKindFromReturn(functionName, thirdArg); if (thirdArgExtract.kind === RecursionType.PlainRecursion) { - // TODO Support multiple conses return { kind: RecursionType.ConsRecursion, - element: secondArg, + elements: [secondArg], arguments: thirdArgExtract.arguments }; } + + if (thirdArgExtract.kind === RecursionType.ConsRecursion) { + thirdArgExtract.elements.push(secondArg); + return thirdArgExtract; + } } return { kind: RecursionType.NotRecursive }; @@ -371,7 +413,7 @@ function createContinuation(label : string, parameterNames : Array, newA ]; } -function createConsContinuation(label : string, parameterNames : Array, element : ts.Expression, newArguments : Array) : Array { +function createConsContinuation(label : string, parameterNames : Array, elements : ts.Expression[], newArguments : Array) : Array { let assignments : Array = []; let reassignments : Array = []; @@ -401,7 +443,7 @@ function createConsContinuation(label : string, parameterNames : Array, }); return [ - addToEnd(element), + ...elements.map(addToEnd), ts.createExpressionStatement( ts.createAssignment( ts.createIdentifier("end"), diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index c2b4b9bf..d35b4157 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -361,6 +361,61 @@ test('should optimize a function that cons (::) on the result of recursive calls expect(actual).toBe(expected); }); +test('should optimize a function that cons (::) on the result of recursive calls (List.map)', () => { + // Corresponds to the following Elm code + // doubleItems list = + // case list of + // [] -> [] + // _ :: xs -> 1 :: 2 :: doubleItems xs + const initialCode = ` + var $something$doubleItems = function (list) { + if (!list.b) { + return _List_Nil; + } else { + var xs = list.b; + return A2( + $elm$core$List$cons, + 1, + A2( + $elm$core$List$cons, + 2, + $something$doubleItems(xs))); + } + }; + `; + + const expectedOutputCode = ` + var $something$doubleItems = function (list) { + var tmp = _List_Cons(undefined, _List_Nil); + var end = tmp; + doubleItems: + while (true) { + if (!list.b) { + return tmp.b; + } else { + var x = list.a; + var xs = list.b; + end.b = _List_Cons(2, _List_Nil); + end = end.b; + end.b = _List_Cons(1, _List_Nil); + end = end.b; + var $temp$list = xs; + list = $temp$list; + continue doubleItems; + } + } + }; + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + export function transformCode( initialCode: string, expectedCode: string, From c8e4355091d20e95086ba5b51b39b9b51f77a725 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Fri, 31 Dec 2021 12:23:04 +0100 Subject: [PATCH 090/229] Rename start/end variables --- src/transforms/tailCallRecursion.ts | 19 +++++--- test/tailCallRecursion.test.ts | 75 ++++------------------------- 2 files changed, 21 insertions(+), 73 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 2d50c4e4..f933fa21 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -198,12 +198,15 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi return recursionType; } +const START = ts.createIdentifier("$start"); +const END = ts.createIdentifier("$end"); + const consDeclarations = [ ts.createVariableStatement( undefined, [ts.createVariableDeclaration( - "tmp", + START, undefined, ts.createCall( ts.createIdentifier("_List_Cons"), @@ -218,9 +221,9 @@ const consDeclarations = ts.createVariableStatement( undefined, [ ts.createVariableDeclaration( - "end", + END, undefined, - ts.createIdentifier("tmp") + START ) ] ) @@ -269,7 +272,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string if (recursionType === RecursionType.ConsRecursion) { const returnStatement = ts.createReturn( ts.createPropertyAccess( - ts.createIdentifier("tmp"), + START, "b" ) ); @@ -282,7 +285,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string ts.createExpressionStatement( ts.createAssignment( ts.createPropertyAccess( - ts.createIdentifier("end"), + END, "b" ), node.expression @@ -446,9 +449,9 @@ function createConsContinuation(label : string, parameterNames : Array, ...elements.map(addToEnd), ts.createExpressionStatement( ts.createAssignment( - ts.createIdentifier("end"), + END, ts.createPropertyAccess( - ts.createIdentifier("end"), + END, "b" ) ) @@ -463,7 +466,7 @@ function addToEnd(element : ts.Expression) : ts.Statement { return ts.createExpressionStatement( ts.createAssignment( ts.createPropertyAccess( - ts.createIdentifier("end"), + END, "b" ), ts.createCall( diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index d35b4157..186d1bab 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -257,17 +257,17 @@ test('should optimize a function that cons (::) on the result of recursive calls const expectedOutputCode = ` var $something$map = F2( function (fn, list) { - var tmp = _List_Cons(undefined, _List_Nil); - var end = tmp; + var $start = _List_Cons(undefined, _List_Nil); + var $end = $start; map: while (true) { if (!list.b) { - return tmp.b; + return $start.b; } else { var x = list.a; var xs = list.b; - end.b = _List_Cons(fn(x), _List_Nil); - end = end.b; + $end.b = _List_Cons(fn(x), _List_Nil); + $end = $end.b; var $temp$list = xs; list = $temp$list; continue map; @@ -327,18 +327,18 @@ test('should optimize a function that cons (::) on the result of recursive calls const expectedOutputCode = ` var $something$filter = F2( function (predicate, list) { - var tmp = _List_Cons(undefined, _List_Nil); - var end = tmp; + var $start = _List_Cons(undefined, _List_Nil); + var $end = $start; filter: while (true) { if (!list.b) { - return tmp.b; + return $start.b; } else { var x = list.a; var xs = list.b; if (predicate(x)) { - end.b = _List_Cons(x, _List_Nil); - end = end.b; + $end.b = _List_Cons(x, _List_Nil); + $end = $end.b; var $temp$list = xs; list = $temp$list; continue filter; @@ -361,61 +361,6 @@ test('should optimize a function that cons (::) on the result of recursive calls expect(actual).toBe(expected); }); -test('should optimize a function that cons (::) on the result of recursive calls (List.map)', () => { - // Corresponds to the following Elm code - // doubleItems list = - // case list of - // [] -> [] - // _ :: xs -> 1 :: 2 :: doubleItems xs - const initialCode = ` - var $something$doubleItems = function (list) { - if (!list.b) { - return _List_Nil; - } else { - var xs = list.b; - return A2( - $elm$core$List$cons, - 1, - A2( - $elm$core$List$cons, - 2, - $something$doubleItems(xs))); - } - }; - `; - - const expectedOutputCode = ` - var $something$doubleItems = function (list) { - var tmp = _List_Cons(undefined, _List_Nil); - var end = tmp; - doubleItems: - while (true) { - if (!list.b) { - return tmp.b; - } else { - var x = list.a; - var xs = list.b; - end.b = _List_Cons(2, _List_Nil); - end = end.b; - end.b = _List_Cons(1, _List_Nil); - end = end.b; - var $temp$list = xs; - list = $temp$list; - continue doubleItems; - } - } - }; - `; - - const { actual, expected } = transformCode( - initialCode, - expectedOutputCode, - createTailCallRecursionTransformer - ); - - expect(actual).toBe(expected); -}); - export function transformCode( initialCode: string, expectedCode: string, From 6ae0bda90da34438828c361e8d0ee32f5e180d11 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Fri, 31 Dec 2021 16:56:02 +0100 Subject: [PATCH 091/229] Reduce the number of parameter reassignments before a continue --- src/transforms/tailCallRecursion.ts | 102 +++++++++++++--------------- test/tailCallRecursion.test.ts | 6 +- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index f933fa21..eb7bf1e2 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -34,6 +34,9 @@ but this version doesn't (because of the additional `<|`): type Context = any; +const LIST_CONS = "_List_Cons"; +const EMPTY_LIST = "_List_Nil"; + export const createTailCallRecursionTransformer : ts.TransformerFactory = (context : Context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.VisitResult => { @@ -209,11 +212,11 @@ const consDeclarations = START, undefined, ts.createCall( - ts.createIdentifier("_List_Cons"), + ts.createIdentifier(LIST_CONS), undefined, [ ts.createIdentifier("undefined"), - ts.createIdentifier("_List_Nil") + ts.createIdentifier(EMPTY_LIST) ] ) )] @@ -277,7 +280,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string ) ); - if (ts.isIdentifier(node.expression) && node.expression.text === "_List_Nil") { + if (ts.isIdentifier(node.expression) && node.expression.text === EMPTY_LIST) { return returnStatement; } @@ -383,82 +386,71 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp } function createContinuation(label : string, parameterNames : Array, newArguments : Array) : Array { - let assignments : Array = []; - let reassignments : Array = []; + return [ + ...paramReassignments(parameterNames, newArguments), + ts.createContinue(label) + ]; +} - parameterNames.forEach((name, index) => { - const correspondingArg : ts.Expression = newArguments[index]; - if (ts.isIdentifier(correspondingArg) - && name === correspondingArg.text - ) { - return; - } - const tempName = `$temp$${name}`; - assignments.push( - ts.createVariableDeclaration( - tempName, - undefined, - newArguments[index] - ) - ); - reassignments.push( +function createConsContinuation(label : string, parameterNames : Array, elements : ts.Expression[], newArguments : Array) : Array { + return [ + ...elements.map(addToEnd), + ts.createExpressionStatement( ts.createAssignment( - ts.createIdentifier(name), - ts.createIdentifier(tempName) + END, + ts.createPropertyAccess( + END, + "b" + ) ) - ); - }); - - return [ - ts.createVariableDeclarationList(assignments), - ...reassignments, + ), + ...paramReassignments(parameterNames, newArguments), ts.createContinue(label) ]; } -function createConsContinuation(label : string, parameterNames : Array, elements : ts.Expression[], newArguments : Array) : Array { +function paramReassignments(parameterNames : Array, newArguments : Array) : Array { let assignments : Array = []; - let reassignments : Array = []; + let reassignments : Array = []; + const filteredParameters : Array<{ name: string; value: ts.Expression; }> = []; parameterNames.forEach((name, index) => { - const correspondingArg : ts.Expression = newArguments[index]; - if (ts.isIdentifier(correspondingArg) - && name === correspondingArg.text - ) { + const value : ts.Expression = newArguments[index]; + if (ts.isIdentifier(value) && name === value.text) { return; } + filteredParameters.push({name, value}); + }); + + if (filteredParameters.length === 1) { + return [ + ts.createAssignment( + ts.createIdentifier(filteredParameters[0].name), + filteredParameters[0].value + ) + ]; + } + + filteredParameters.forEach(({name, value}) => { const tempName = `$temp$${name}`; assignments.push( ts.createVariableDeclaration( tempName, undefined, - newArguments[index] + value ) ); reassignments.push( - ts.createExpressionStatement( - ts.createAssignment( - ts.createIdentifier(name), - ts.createIdentifier(tempName) - ) + ts.createAssignment( + ts.createIdentifier(name), + ts.createIdentifier(tempName) ) ); }); return [ - ...elements.map(addToEnd), - ts.createExpressionStatement( - ts.createAssignment( - END, - ts.createPropertyAccess( - END, - "b" - ) - ) - ), ts.createVariableDeclarationList(assignments), - ...reassignments, - ts.createContinue(label) + ...reassignments ]; } @@ -470,11 +462,11 @@ function addToEnd(element : ts.Expression) : ts.Statement { "b" ), ts.createCall( - ts.createIdentifier("_List_Cons"), + ts.createIdentifier(LIST_CONS), undefined, [ element, - ts.createIdentifier("_List_Nil") + ts.createIdentifier(EMPTY_LIST) ] ) ) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 186d1bab..7f6d0af6 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -268,8 +268,7 @@ test('should optimize a function that cons (::) on the result of recursive calls var xs = list.b; $end.b = _List_Cons(fn(x), _List_Nil); $end = $end.b; - var $temp$list = xs; - list = $temp$list; + list = xs; continue map; } } @@ -339,8 +338,7 @@ test('should optimize a function that cons (::) on the result of recursive calls if (predicate(x)) { $end.b = _List_Cons(x, _List_Nil); $end = $end.b; - var $temp$list = xs; - list = $temp$list; + list = xs; continue filter; } else { var $temp$list = xs; From 8b25befcbf9390b80f103950ddb5735f26e42570 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Fri, 31 Dec 2021 17:55:29 +0100 Subject: [PATCH 092/229] Update TODO --- src/transforms/tailCallRecursion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index eb7bf1e2..66ede2f1 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -30,7 +30,7 @@ but this version doesn't (because of the additional `<|`): // TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors -// TODO Enable TCO for let declarations +// TODO Enable TCO for let declarations (watch out for name shadowing for $start/$end for nested recursive functions) type Context = any; From 1079efa4f2f81d5d92adc254bd13ac5bd8dc2f8a Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Sat, 1 Jan 2022 02:12:59 +0100 Subject: [PATCH 093/229] Support recursion even with boolean logical operators --- src/transforms/tailCallRecursion.ts | 193 ++++++++++++++++++++-------- test/tailCallRecursion.test.ts | 109 +++++++++++++++- 2 files changed, 249 insertions(+), 53 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 66ede2f1..799e6a0c 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -28,7 +28,6 @@ but this version doesn't (because of the additional `<|`): */ -// TODO Enable TCO for code like `rec n = if ... then False else condition n && rec (n - 1)`, using `&&` or `||` // TODO Enable TCO for other kinds of data constructors // TODO Enable TCO for let declarations (watch out for name shadowing for $start/$end for nested recursive functions) @@ -147,6 +146,12 @@ enum RecursionType { NotRecursive = 0, PlainRecursion = 1, ConsRecursion = 2, + BooleanRecursion = 3, +}; + +enum BooleanKind { + And, + Or, }; type Recursion @@ -154,6 +159,7 @@ type Recursion | { kind: RecursionType.PlainRecursion, arguments : Array } // Could hold a Recursion as data | { kind: RecursionType.ConsRecursion, elements : ts.Expression[], arguments : Array } + | { kind: RecursionType.BooleanRecursion, expression: ts.Expression, mainOperator: BooleanKind, arguments : Array } function determineRecursionType(functionName : string, body : ts.Node) : RecursionType { let recursionType : RecursionType = RecursionType.NotRecursive; @@ -161,6 +167,11 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi let node : ts.Node | undefined; loop: while (recursionType <= 1 && (node = nodesToVisit.shift())) { + if (ts.isParenthesizedExpression(node)) { + nodesToVisit = [node.expression, ...nodesToVisit]; + continue loop; + } + if (ts.isBlock(node)) { nodesToVisit = [...node.statements, ...nodesToVisit]; continue loop; @@ -186,12 +197,9 @@ function determineRecursionType(functionName : string, body : ts.Node) : Recursi continue loop; } - if (ts.isReturnStatement(node) - && node.expression - && ts.isCallExpression(node.expression) - ) { + if (ts.isReturnStatement(node) && node.expression) { recursionType = Math.max( - extractRecursionKindFromReturn(functionName, node.expression).kind, + extractRecursionKindFromExpression(functionName, node.expression).kind, recursionType ); continue loop; @@ -254,49 +262,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string if (ts.isReturnStatement(node) && node.expression ) { - if (ts.isCallExpression(node.expression)) { - const extract = extractRecursionKindFromReturn(functionName, node.expression); - - switch (extract.kind) { - case RecursionType.NotRecursive: { - return node; - } - - case RecursionType.PlainRecursion: { - return createContinuation(label, parameterNames, extract.arguments); - } - - case RecursionType.ConsRecursion: { - return createConsContinuation(label, parameterNames, extract.elements, extract.arguments); - } - } - } - - if (recursionType === RecursionType.ConsRecursion) { - const returnStatement = ts.createReturn( - ts.createPropertyAccess( - START, - "b" - ) - ); - - if (ts.isIdentifier(node.expression) && node.expression.text === EMPTY_LIST) { - return returnStatement; - } - - return [ - ts.createExpressionStatement( - ts.createAssignment( - ts.createPropertyAccess( - END, - "b" - ), - node.expression - ) - ), - returnStatement - ]; - } + return updateReturnStatement(recursionType, functionName, label, parameterNames, node.expression) || node; } return node; @@ -336,7 +302,82 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string return updatedBlock; } -function extractRecursionKindFromReturn(functionName : string, node : ts.CallExpression) : Recursion { +function updateReturnStatement(recursionType : RecursionType, functionName : string, label : string, parameterNames : Array, expression : ts.Expression) { + const extract = extractRecursionKindFromExpression(functionName, expression); + + if (recursionType === RecursionType.ConsRecursion) { + if (extract.kind === RecursionType.PlainRecursion) { + return createContinuation(label, parameterNames, extract.arguments); + } + + if (extract.kind === RecursionType.ConsRecursion) { + return createConsContinuation(label, parameterNames, extract.elements, extract.arguments); + } + + // End of the cons recursion, add the value to the head of the list and return it. + + // `return $end.b` + const returnStatement = ts.createReturn( + ts.createPropertyAccess( + START, + "b" + ) + ); + + if (ts.isIdentifier(expression) && expression.text === EMPTY_LIST) { + // The end of the list is already an empty list, setting it would be useless. + return returnStatement; + } + + return [ + // `$end.b = ` + ts.createExpressionStatement( + ts.createAssignment( + ts.createPropertyAccess( + END, + "b" + ), + expression + ) + ), + returnStatement + ]; + } + + switch (extract.kind) { + case RecursionType.NotRecursive: { + return null; + } + + case RecursionType.PlainRecursion: { + return createContinuation(label, parameterNames, extract.arguments); + } + + case RecursionType.BooleanRecursion: { + return createBooleanContinuation(label, parameterNames, extract.mainOperator, extract.expression, extract.arguments); + } + } + + return null; +} + +function extractRecursionKindFromExpression(functionName : string, node : ts.Expression) : Recursion { + if (ts.isParenthesizedExpression(node)) { + return extractRecursionKindFromExpression(functionName, node.expression); + } + + if (ts.isCallExpression(node)) { + return extractRecursionKindFromCallExpression(functionName, node); + } + + if (ts.isBinaryExpression(node) && node.operatorToken) { + return extractRecursionKindFromBinaryExpression(functionName, node); + } + + return { kind: RecursionType.NotRecursive }; +} + +function extractRecursionKindFromCallExpression(functionName : string, node : ts.CallExpression) : Recursion { if (!ts.isIdentifier(node.expression)) { return { kind: RecursionType.NotRecursive }; } @@ -367,7 +408,7 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp // Elm: Is x :: // JS: Is A2($elm$core$List$cons, x, ) if (firstArg.text === "$elm$core$List$cons" && ts.isCallExpression(thirdArg)) { - const thirdArgExtract = extractRecursionKindFromReturn(functionName, thirdArg); + const thirdArgExtract = extractRecursionKindFromExpression(functionName, thirdArg); if (thirdArgExtract.kind === RecursionType.PlainRecursion) { return { kind: RecursionType.ConsRecursion, @@ -385,6 +426,30 @@ function extractRecursionKindFromReturn(functionName : string, node : ts.CallExp return { kind: RecursionType.NotRecursive }; } +function extractRecursionKindFromBinaryExpression(functionName : string, node : ts.BinaryExpression) : Recursion { + if (node.operatorToken.kind !== ts.SyntaxKind.BarBarToken && node.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return { kind: RecursionType.NotRecursive }; + } + + const extract = extractRecursionKindFromExpression(functionName, node.right); + + if (extract.kind === RecursionType.PlainRecursion) { + return { + kind: RecursionType.BooleanRecursion, + expression: node.left, + mainOperator: node.operatorToken.kind === ts.SyntaxKind.BarBarToken ? BooleanKind.Or : BooleanKind.And, + arguments: extract.arguments + }; + } + + if (extract.kind === RecursionType.BooleanRecursion) { + extract.expression = ts.createBinary(node.left, node.operatorToken, extract.expression); + return extract; + } + + return { kind: RecursionType.NotRecursive }; +} + function createContinuation(label : string, parameterNames : Array, newArguments : Array) : Array { return [ ...paramReassignments(parameterNames, newArguments), @@ -409,6 +474,31 @@ function createConsContinuation(label : string, parameterNames : Array, ]; } +function createBooleanContinuation(label : string, parameterNames : Array, mainOperator: BooleanKind, expression : ts.Expression, newArguments : Array) : Array { + const ifExpr = + mainOperator === BooleanKind.Or + ? // if () { return true; } + ts.createIf( + expression, + ts.createBlock([ + ts.createReturn(ts.createTrue()) + ]) + ) + : // if (!) { return false; } + ts.createIf( + ts.createLogicalNot(expression), + ts.createBlock([ + ts.createReturn(ts.createFalse()) + ]) + ); + + return [ + ifExpr, + ...paramReassignments(parameterNames, newArguments), + ts.createContinue(label) + ]; +} + function paramReassignments(parameterNames : Array, newArguments : Array) : Array { let assignments : Array = []; let reassignments : Array = []; @@ -455,6 +545,7 @@ function paramReassignments(parameterNames : Array, newArguments : Array } function addToEnd(element : ts.Expression) : ts.Statement { + // `return end.b = _List_Cons(element, _List_Nil);` return ts.createExpressionStatement( ts.createAssignment( ts.createPropertyAccess( diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index 7f6d0af6..d38cd2e2 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -290,7 +290,7 @@ test('should optimize a function that cons (::) on the result of recursive calls // filter predicate list = // case list of // [] -> - // [] + // something 0 // // x :: xs -> // if predicate x then @@ -304,7 +304,7 @@ test('should optimize a function that cons (::) on the result of recursive calls filter: while (true) { if (!list.b) { - return _List_Nil; + return something(0); } else { var x = list.a; var xs = list.b; @@ -331,6 +331,7 @@ test('should optimize a function that cons (::) on the result of recursive calls filter: while (true) { if (!list.b) { + $end.b = something(0); return $start.b; } else { var x = list.a; @@ -359,6 +360,110 @@ test('should optimize a function that cons (::) on the result of recursive calls expect(actual).toBe(expected); }); +test('should optimize a function that does "x || "', () => { + // Corresponds to the following Elm code + // naiveAny : (a -> Bool) -> List a -> Bool + // naiveAny isOkay list = + // case list of + // [] -> + // False + // + // x :: xs -> + // isOkay x || False || naiveAny isOkay xs + const initialCode = ` + var $something$naiveAny = F2( + function (isOkay, list) { + if (!list.b) { + return false; + } else { + var x = list.a; + var xs = list.b; + return isOkay(x) || (false || A2($something$naiveAny, isOkay, xs)); + } + }); + `; + + const expectedOutputCode = ` + var $something$naiveAny = F2( + function (isOkay, list) { + naiveAny: + while (true) { + if (!list.b) { + return false; + } else { + var x = list.a; + var xs = list.b; + if (isOkay(x) || false) { + return true; + } + list = xs; + continue naiveAny; + } + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + +test('should optimize a function that does "x && "', () => { + // Corresponds to the following Elm code + // naiveAll : (a -> Bool) -> List a -> Bool + // naiveAll isOkay list = + // case list of + // [] -> + // True + // + // x :: xs -> + // isOkay x && True && naiveAll isOkay xs + const initialCode = ` + var $something$naiveAll = F2( + function (isOkay, list) { + if (!list.b) { + return true; + } else { + var x = list.a; + var xs = list.b; + return isOkay(x) && (true && A2($something$naiveAll, isOkay, xs)); + } + }); + `; + + const expectedOutputCode = ` + var $something$naiveAll = F2( + function (isOkay, list) { + naiveAll: + while (true) { + if (!list.b) { + return true; + } else { + var x = list.a; + var xs = list.b; + if (!(isOkay(x) && true)) { + return false; + } + list = xs; + continue naiveAll; + } + } + }); + `; + + const { actual, expected } = transformCode( + initialCode, + expectedOutputCode, + createTailCallRecursionTransformer + ); + + expect(actual).toBe(expected); +}); + export function transformCode( initialCode: string, expectedCode: string, From 4788afe03d54e24ce53246d2c6ecea52eb94bc2d Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Sat, 1 Jan 2022 20:50:44 +0100 Subject: [PATCH 094/229] Test formatting --- test/tailCallRecursion.test.ts | 183 +++++++++++++++++---------------- 1 file changed, 95 insertions(+), 88 deletions(-) diff --git a/test/tailCallRecursion.test.ts b/test/tailCallRecursion.test.ts index d38cd2e2..28d9955b 100644 --- a/test/tailCallRecursion.test.ts +++ b/test/tailCallRecursion.test.ts @@ -117,36 +117,40 @@ test('should re-use the label and while loop if there already is one', () => { // recursiveFunction mapper xs <| (mapper x :: acc) const initialCode = ` var something$recursiveFunction = F3( - function (mapper, list, acc) { - recursiveFunction: - while (true) { - if (!list.b) { - return acc; - } else if (cond) { - var x = list.a; - var xs = list.b; - var $temp$mapper = mapper, - $temp$list = xs, - $temp$acc = A2( - $elm$core$List$cons, - mapper(x), - acc); - mapper = $temp$mapper; - list = $temp$list; - acc = $temp$acc; - continue recursiveFunction; - } else { - return A3( - something$recursiveFunction, - mapper, - xs, - A2( - $elm$core$List$cons, - mapper(x), - acc)); - } - } - }); + function (mapper, list, acc) { + recursiveFunction: + while (true) { + if (!list.b) { + return acc; + } else { + var x = list.a; + var xs = list.b; + if (cond) { + var x = list.a; + var xs = list.b; + var $temp$mapper = mapper, + $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + mapper = $temp$mapper; + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } else { + return A3( + something$recursiveFunction, + mapper, + xs, + A2( + $elm$core$List$cons, + mapper(x), + acc)); + } + } + } + }); `; // Corresponds to the following TCO-ed Elm code @@ -159,38 +163,40 @@ test('should re-use the label and while loop if there already is one', () => { // recursiveFunction mapper xs (mapper x :: acc) const expectedOutputCode = ` var something$recursiveFunction = F3( - function (mapper, list, acc) { - recursiveFunction: - while (true) { - if (!list.b) { - return acc; - } else if (cond) { - var x = list.a; - var xs = list.b; - var $temp$mapper = mapper, - $temp$list = xs, - $temp$acc = A2( - $elm$core$List$cons, - mapper(x), - acc); - mapper = $temp$mapper; - list = $temp$list; - acc = $temp$acc; - continue recursiveFunction; - } else { - var x = list.a; - var xs = list.b; - var $temp$list = xs, - $temp$acc = A2( - $elm$core$List$cons, - mapper(x), - acc); - list = $temp$list; - acc = $temp$acc; - continue recursiveFunction; - } - } - }); + function (mapper, list, acc) { + recursiveFunction: + while (true) { + if (!list.b) { + return acc; + } else { + var x = list.a; + var xs = list.b; + if (cond) { + var x = list.a; + var xs = list.b; + var $temp$mapper = mapper, + $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + mapper = $temp$mapper; + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } else { + var $temp$list = xs, + $temp$acc = A2( + $elm$core$List$cons, + mapper(x), + acc); + list = $temp$list; + acc = $temp$acc; + continue recursiveFunction; + } + } + } + }); `; const { actual, expected } = transformCode( @@ -235,9 +241,10 @@ test('should not change non-recursive functions', () => { test('should optimize a function that cons (::) on the result of recursive calls (List.map)', () => { // Corresponds to the following Elm code // map fn list = - // case list of - // [] -> [] - // x :: xs -> fn x :: map fn xs + // case list of + // [] -> [] + // x :: xs -> + // fn x :: map fn xs const initialCode = ` var $something$map = F2( function (fn, list) { @@ -325,30 +332,30 @@ test('should optimize a function that cons (::) on the result of recursive calls const expectedOutputCode = ` var $something$filter = F2( - function (predicate, list) { - var $start = _List_Cons(undefined, _List_Nil); - var $end = $start; - filter: - while (true) { - if (!list.b) { + function (predicate, list) { + var $start = _List_Cons(undefined, _List_Nil); + var $end = $start; + filter: + while (true) { + if (!list.b) { $end.b = something(0); - return $start.b; - } else { - var x = list.a; - var xs = list.b; - if (predicate(x)) { - $end.b = _List_Cons(x, _List_Nil); - $end = $end.b; - list = xs; - continue filter; - } else { - var $temp$list = xs; - list = $temp$list; - continue filter; - } - } - } - }); + return $start.b; + } else { + var x = list.a; + var xs = list.b; + if (predicate(x)) { + $end.b = _List_Cons(x, _List_Nil); + $end = $end.b; + list = xs; + continue filter; + } else { + var $temp$list = xs; + list = $temp$list; + continue filter; + } + } + } + }); `; const { actual, expected } = transformCode( From 6a918b91be1a272d20244822e8393bcc9348ef94 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Sat, 1 Jan 2022 21:05:49 +0100 Subject: [PATCH 095/229] Add explanation comments --- src/transforms/tailCallRecursion.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/transforms/tailCallRecursion.ts b/src/transforms/tailCallRecursion.ts index 799e6a0c..15f2940e 100644 --- a/src/transforms/tailCallRecursion.ts +++ b/src/transforms/tailCallRecursion.ts @@ -214,6 +214,7 @@ const END = ts.createIdentifier("$end"); const consDeclarations = [ + // `var $start = _List_Cons(undefined, _List_Nil);` ts.createVariableStatement( undefined, [ts.createVariableDeclaration( @@ -229,6 +230,7 @@ const consDeclarations = ) )] ), + // `var $end = $start;` ts.createVariableStatement( undefined, [ ts.createVariableDeclaration( @@ -275,6 +277,7 @@ function updateFunctionBody(recursionType : RecursionType, functionName : string if (recursionType === RecursionType.PlainRecursion) { if (!ts.isLabeledStatement(updatedBlock.statements[0])) { return ts.createBlock( + // `