├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── cmd.js └── usage.txt ├── example.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.bundle.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "iojs" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Alexander Gugel 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://img.shields.io/badge/stability-experimental-orange.svg?style=flat) 2 | [![Build Status](https://travis-ci.org/alexanderGugel/tailcall.svg?branch=master)](https://travis-ci.org/alexanderGugel/tailcall) 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 4 | 5 | # tailcall 6 | 7 | `tailcall` is a browserify transform and command line utility that can be used for eliminating tail calls in recursive functions (TCO = tail call optimization). This prevents excessive growth of the used call stack and generally speaking increases performance (in most cases). 8 | 9 | Tail call optimization is are part of the ECMAScript 6 spec: 10 | [Tail call optimization in ECMAScript 6 11 | ](http://www.2ality.com/2015/06/tail-call-optimization.html) 12 | 13 | `tailcall` uses [`acorn`](https://www.npmjs.com/package/acorn) to generate and traverse the AST. 14 | 15 | ## Example 16 | 17 | Input (tail recursive factorial function): 18 | 19 | ```js 20 | function fact (n, acc) { 21 | acc = acc != null ? acc : 1 22 | if (n < 2) return 1 * acc 23 | return fact(n - 1, acc * n) 24 | } 25 | ``` 26 | 27 | Output (no more recursive tail calls): 28 | 29 | ```js 30 | function fact(n, acc) { 31 | var __n, __acc, __; 32 | while (true) { 33 | acc = acc != null ? acc : 1; 34 | __n = n - 1; 35 | __acc = acc * n; 36 | n = __n; 37 | acc = __acc; 38 | if (n < 2) 39 | return 1 * acc; 40 | } 41 | } 42 | ``` 43 | 44 | ## Install 45 | 46 | With [npm](https://npmjs.org) do: 47 | 48 | ``` 49 | npm install tailcall 50 | ``` 51 | 52 | ## Usage via [`browserify`](https://github.com/substack/node-browserify) 53 | 54 | ``` 55 | browserify -t tailcall index.js 56 | ``` 57 | 58 | or add it to your `package.json`: 59 | 60 | ```json 61 | { 62 | "browserify": { 63 | "transform": ["tailcall"] 64 | } 65 | } 66 | ``` 67 | 68 | ## CLI 69 | 70 | Usage of the command line utility usually requires a global install (via `npm i -g tailcall`) or a npm script. 71 | 72 | ``` 73 | tailcall index.js 74 | ``` 75 | 76 | ``` 77 | usage: 78 | 79 | tailcall file 80 | 81 | Eliminate tail recursive function calls from `file`, printing the 82 | transformed file contents to stdout. 83 | 84 | tailcall 85 | tailcall - 86 | 87 | Eliminate tail recursive function calls from `file`, printing the 88 | transformed file contents to stdout. 89 | ``` 90 | 91 | ## License 92 | 93 | Licensed under the ISC license. See `LICENSE` file for further info. 94 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var brfs = require('../') 6 | 7 | var file = process.argv[2] 8 | 9 | if (file === '-h' || file === '--help') { 10 | fs.createReadStream(path.join(__dirname, 'usage.txt')) 11 | .pipe(process.stdout) 12 | } else { 13 | var fromFile = file && file !== '-' 14 | var rs = fromFile 15 | ? fs.createReadStream(file) 16 | : process.stdin 17 | 18 | var fpath = fromFile ? file : path.join(process.cwd(), '-') 19 | rs.pipe(brfs(fpath)).pipe(process.stdout) 20 | } 21 | -------------------------------------------------------------------------------- /bin/usage.txt: -------------------------------------------------------------------------------- 1 | usage: 2 | 3 | tailcall file 4 | 5 | Eliminate tail recursive function calls from `file`, printing the 6 | transformed file contents to stdout. 7 | 8 | tailcall 9 | tailcall - 10 | 11 | Eliminate tail recursive function calls from `file`, printing the 12 | transformed file contents to stdout. 13 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | function fact (n, acc) { 2 | acc = acc != null ? acc : 1 3 | if (n < 2) return 1 * acc 4 | return fact(n - 1, acc * n) 5 | } 6 | 7 | for (var n = 0; n < 10; n++) { 8 | console.log(n + ': ' + fact(n)) 9 | } 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var acorn = require('acorn') 2 | var walk = require('acorn/dist/walk') 3 | var through = require('through') 4 | var path = require('path') 5 | var escodegen = require('escodegen') 6 | 7 | function eliminateTailCalls (ast) { 8 | 9 | var optimizeableFunctionDeclarationNodes = [] 10 | 11 | walk.ancestor(ast, { 12 | CallExpression: function (node, ancestors) { 13 | if ( 14 | ancestors.length < 3 || 15 | ancestors[ancestors.length - 2].type !== 'ReturnStatement' 16 | ) return 17 | 18 | var returnStatementNode = ancestors[ancestors.length - 2] 19 | 20 | var functionDeclarationNode = null 21 | 22 | for (var i = ancestors.length - 1; i >= 0; i--) { 23 | if ( 24 | ancestors[i].type === 'FunctionDeclaration' && 25 | ancestors[i].id.name === node.callee.name 26 | ) { 27 | functionDeclarationNode = ancestors[i] 28 | if (functionDeclarationNode) break 29 | } 30 | } 31 | 32 | if (!functionDeclarationNode) return 33 | 34 | if (!functionDeclarationNode.__markedAsOptimizeable__) { 35 | functionDeclarationNode.__markedAsOptimizeable__ = true 36 | optimizeableFunctionDeclarationNodes.push(functionDeclarationNode) 37 | } 38 | 39 | var returnStatementNodeIndex = ancestors[ancestors.length - 3].body.indexOf(returnStatementNode) 40 | 41 | var args = [ 42 | returnStatementNodeIndex, 43 | 1 44 | ] 45 | 46 | for ( 47 | var j = 0; 48 | j < Math.max(functionDeclarationNode.params.length, node.arguments.length); 49 | j++ 50 | ) { 51 | var param = functionDeclarationNode.params[j] 52 | var arg = node.arguments[j] 53 | 54 | if (!param) { 55 | param = { name: '' } 56 | } 57 | 58 | if (!arg) { 59 | arg = { 60 | type: 'Identifier', 61 | start: 12, 62 | end: 21, 63 | name: 'undefined' 64 | } 65 | } 66 | 67 | args.push({ 68 | type: 'ExpressionStatement', 69 | expression: { 70 | type: 'AssignmentExpression', 71 | operator: '=', 72 | left: { type: 'Identifier', name: '__' + param.name }, 73 | right: arg 74 | } 75 | }) 76 | } 77 | 78 | for ( 79 | var k = 0; 80 | k < Math.min(functionDeclarationNode.params.length, node.arguments.length); 81 | k++ 82 | ) { 83 | param = functionDeclarationNode.params[k] 84 | 85 | args.push({ 86 | type: 'ExpressionStatement', 87 | expression: { 88 | type: 'AssignmentExpression', 89 | operator: '=', 90 | left: { type: 'Identifier', name: param.name }, 91 | right: { type: 'Identifier', name: '__' + param.name } 92 | } 93 | }) 94 | } 95 | 96 | var returnStatementNodeAncestorBody = ancestors[ancestors.length - 3].body 97 | var injectContinue = returnStatementNodeIndex < returnStatementNodeAncestorBody.length - 1 98 | 99 | returnStatementNodeAncestorBody.splice.apply(returnStatementNodeAncestorBody, args) 100 | 101 | if (injectContinue) { 102 | args.push({ type: 'ContinueStatement' }) 103 | } 104 | 105 | } 106 | }) 107 | 108 | optimizeableFunctionDeclarationNodes.forEach(function (node) { 109 | var blockStatementBody = node.body.body 110 | 111 | node.body.body = [{ 112 | type: 'VariableDeclaration', 113 | declarations: (node.params.map(function (param) { 114 | return param.name 115 | }).concat([''])).map(function (name) { 116 | return { 117 | type: 'VariableDeclarator', 118 | id: { 119 | type: 'Identifier', 120 | name: '__' + name 121 | } 122 | } 123 | }), 124 | kind: 'var' 125 | }, 126 | { 127 | type: 'WhileStatement', 128 | test: { 129 | type: 'Literal', 130 | value: true, 131 | raw: 'true' 132 | }, 133 | body: { 134 | type: 'BlockStatement', 135 | body: blockStatementBody 136 | } 137 | }] 138 | }) 139 | 140 | return ast 141 | } 142 | 143 | function transform (file) { 144 | if (path.extname(file) !== '.js') return through() 145 | 146 | var source = '' 147 | var stream = through(write, end) 148 | 149 | function write (buf) { 150 | source += buf 151 | } 152 | 153 | function end () { 154 | try { 155 | var ast = acorn.parse(source) 156 | 157 | ast = eliminateTailCalls(ast) 158 | 159 | var code = escodegen.generate(ast) 160 | this.queue(code) 161 | } catch (e) { 162 | return stream.emit('error', e) 163 | } 164 | 165 | this.queue(null) 166 | } 167 | 168 | return stream 169 | } 170 | 171 | module.exports = transform 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailcall", 3 | "version": "0.0.0", 4 | "description": "Eliminate tail recursive function calls", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard", 8 | "example": "browserify -t ./index.js -e example.js -o example.bundle.js && node example.bundle.js" 9 | }, 10 | "author": "Alexander Gugel ", 11 | "license": "ISC", 12 | "dependencies": { 13 | "acorn": "^2.1.0", 14 | "escodegen": "^1.6.1", 15 | "through": "^2.3.8" 16 | }, 17 | "devDependencies": { 18 | "browserify": "^11.0.0", 19 | "standard": "^4.5.4" 20 | }, 21 | "bin": { 22 | "brfs": "bin/cmd.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/alexanderGugel/tailcall.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/alexanderGugel/tailcall/issues" 30 | }, 31 | "homepage": "https://github.com/alexanderGugel/tailcall#readme", 32 | "keywords": [ 33 | "browserify-transform", 34 | "browserify", 35 | "ast", 36 | "acorn", 37 | "perf", 38 | "optimization", 39 | "recursion", 40 | "es6", 41 | "shim" 42 | ] 43 | } 44 | --------------------------------------------------------------------------------