From 87226b6f44f6c6cc9c0da8f65c93c9f747fb9ce6 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sat, 24 Jul 2010 12:12:26 -0700 Subject: [PATCH] fixing bound functions with an __bind helper. --- lib/nodes.js | 22 +- lib/rewriter.js | 414 ++++++++++++++++++------------------- src/nodes.coffee | 23 +-- test/test_functions.coffee | 14 +- 4 files changed, 229 insertions(+), 244 deletions(-) diff --git a/lib/nodes.js b/lib/nodes.js index 46168b6a..f9b9c1f9 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -203,11 +203,6 @@ this.expressions[idx] = last.makeReturn(); return this; }; - Expressions.prototype.rewriteThis = function() { - return this.traverseChildren(false, function(child) { - return child instanceof ValueNode && child.base.value === 'this' ? (child.base = literal('__this')) : null; - }); - }; Expressions.prototype.compile = function(o) { o = o || {}; return o.scope ? Expressions.__superClass__.compile.call(this, o) : this.compileRoot(o); @@ -952,7 +947,7 @@ top = del(o, 'top'); o.scope = sharedScope || new Scope(o.scope, this.body, this); o.top = true; - o.indent = this.idt(this.bound ? 2 : 1); + o.indent = this.idt(1); del(o, 'noWrap'); del(o, 'globals'); i = 0; @@ -983,23 +978,17 @@ return _e; })(); this.body.makeReturn(); - if (this.bound) { - this.body.rewriteThis(); - } _j = params; for (_i = 0, _k = _j.length; _i < _k; _i++) { param = _j[_i]; (o.scope.parameter(param)); } code = this.body.expressions.length ? ("\n" + (this.body.compileWithDeclarations(o)) + "\n") : ''; - func = ("function(" + (params.join(', ')) + ") {" + code + (code && this.idt(this.bound ? 1 : 0)) + "}"); - if (top && !this.bound) { - func = ("(" + func + ")"); + func = ("function(" + (params.join(', ')) + ") {" + code + (code && this.tab) + "}"); + if (this.bound) { + return ("" + (utility('bind')) + "(" + func + ", this)"); } - if (!(this.bound)) { - return func; - } - return "(function(__this) {\n" + (this.idt(1)) + "return " + func + ";\n" + this.tab + "})(this)"; + return top ? ("(" + func + ")") : func; }; CodeNode.prototype.topSensitive = function() { return true; @@ -1682,6 +1671,7 @@ }); UTILITIES = { 'extends': "function(child, parent) {\n var ctor = function(){};\n ctor.prototype = parent.prototype;\n child.prototype = new ctor();\n child.prototype.constructor = child;\n if (typeof parent.extended === \"function\") parent.extended(child);\n child.__superClass__ = parent.prototype;\n }", + bind: "function(func, context) {\n return function(){ return func.apply(context, arguments); };\n }", hasProp: 'Object.prototype.hasOwnProperty', slice: 'Array.prototype.slice' }; diff --git a/lib/rewriter.js b/lib/rewriter.js index ee180171..de8d43b0 100644 --- a/lib/rewriter.js +++ b/lib/rewriter.js @@ -1,6 +1,8 @@ (function() { var BALANCED_PAIRS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_BLOCK, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, INVERSES, Rewriter, SINGLE_CLOSERS, SINGLE_LINERS, _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, helpers, include, pair; - var __hasProp = Object.prototype.hasOwnProperty; + var __bind = function(func, context) { + return function(){ return func.apply(context, arguments); }; + }, __hasProp = Object.prototype.hasOwnProperty; if (typeof process !== "undefined" && process !== null) { _a = require('./helpers'); helpers = _a.helpers; @@ -37,30 +39,28 @@ return true; }; Rewriter.prototype.adjustComments = function() { - return this.scanTokens((function(__this) { - return function(prev, token, post, i) { - var _c, _d, after, before; - if (!(token[0] === 'HERECOMMENT')) { - return 1; - } - _c = [__this.tokens[i - 2], __this.tokens[i + 2]]; - before = _c[0]; - after = _c[1]; - if (after && after[0] === 'INDENT') { - __this.tokens.splice(i + 2, 1); - before && before[0] === 'OUTDENT' && post && (prev[0] === post[0]) && (post[0] === 'TERMINATOR') ? __this.tokens.splice(i - 2, 1) : __this.tokens.splice(i, 0, after); - } else if (prev && !('TERMINATOR' === (_d = prev[0]) || 'INDENT' === _d || 'OUTDENT' === _d)) { - if (post && post[0] === 'TERMINATOR' && after && after[0] === 'OUTDENT') { - __this.tokens.splice.apply(__this.tokens, [i + 2, 0].concat(__this.tokens.splice(i, 2))); - __this.tokens[i + 2][0] !== 'TERMINATOR' ? __this.tokens.splice(i + 2, 0, ['TERMINATOR', "\n", prev[2]]) : null; - } else { - __this.tokens.splice(i, 0, ['TERMINATOR', "\n", prev[2]]); - } - return 2; - } + return this.scanTokens(__bind(function(prev, token, post, i) { + var _c, _d, after, before; + if (!(token[0] === 'HERECOMMENT')) { return 1; - }; - })(this)); + } + _c = [this.tokens[i - 2], this.tokens[i + 2]]; + before = _c[0]; + after = _c[1]; + if (after && after[0] === 'INDENT') { + this.tokens.splice(i + 2, 1); + before && before[0] === 'OUTDENT' && post && (prev[0] === post[0]) && (post[0] === 'TERMINATOR') ? this.tokens.splice(i - 2, 1) : this.tokens.splice(i, 0, after); + } else if (prev && !('TERMINATOR' === (_d = prev[0]) || 'INDENT' === _d || 'OUTDENT' === _d)) { + if (post && post[0] === 'TERMINATOR' && after && after[0] === 'OUTDENT') { + this.tokens.splice.apply(this.tokens, [i + 2, 0].concat(this.tokens.splice(i, 2))); + this.tokens[i + 2][0] !== 'TERMINATOR' ? this.tokens.splice(i + 2, 0, ['TERMINATOR', "\n", prev[2]]) : null; + } else { + this.tokens.splice(i, 0, ['TERMINATOR', "\n", prev[2]]); + } + return 2; + } + return 1; + }, this)); }; Rewriter.prototype.removeLeadingNewlines = function() { var _c; @@ -71,192 +71,180 @@ return _c; }; Rewriter.prototype.removeMidExpressionNewlines = function() { - return this.scanTokens((function(__this) { - return function(prev, token, post, i) { - if (!(post && include(EXPRESSION_CLOSE, post[0]) && token[0] === 'TERMINATOR')) { - return 1; - } - __this.tokens.splice(i, 1); - return 0; - }; - })(this)); + return this.scanTokens(__bind(function(prev, token, post, i) { + if (!(post && include(EXPRESSION_CLOSE, post[0]) && token[0] === 'TERMINATOR')) { + return 1; + } + this.tokens.splice(i, 1); + return 0; + }, this)); }; Rewriter.prototype.closeOpenCallsAndIndexes = function() { var brackets, parens; parens = [0]; brackets = [0]; - return this.scanTokens((function(__this) { - return function(prev, token, post, i) { - var _c; - if ((_c = token[0]) === 'CALL_START') { - parens.push(0); - } else if (_c === 'INDEX_START') { - brackets.push(0); - } else if (_c === '(') { - parens[parens.length - 1] += 1; - } else if (_c === '[') { - brackets[brackets.length - 1] += 1; - } else if (_c === ')') { - if (parens[parens.length - 1] === 0) { - parens.pop(); - token[0] = 'CALL_END'; - } else { - parens[parens.length - 1] -= 1; - } - } else if (_c === ']') { - if (brackets[brackets.length - 1] === 0) { - brackets.pop(); - token[0] = 'INDEX_END'; - } else { - brackets[brackets.length - 1] -= 1; - } + return this.scanTokens(__bind(function(prev, token, post, i) { + var _c; + if ((_c = token[0]) === 'CALL_START') { + parens.push(0); + } else if (_c === 'INDEX_START') { + brackets.push(0); + } else if (_c === '(') { + parens[parens.length - 1] += 1; + } else if (_c === '[') { + brackets[brackets.length - 1] += 1; + } else if (_c === ')') { + if (parens[parens.length - 1] === 0) { + parens.pop(); + token[0] = 'CALL_END'; + } else { + parens[parens.length - 1] -= 1; } - return 1; - }; - })(this)); + } else if (_c === ']') { + if (brackets[brackets.length - 1] === 0) { + brackets.pop(); + token[0] = 'INDEX_END'; + } else { + brackets[brackets.length - 1] -= 1; + } + } + return 1; + }, this)); }; Rewriter.prototype.addImplicitParentheses = function() { var closeCalls, stack; stack = [0]; - closeCalls = (function(__this) { - return function(i) { - var _c, size, tmp; - _c = stack[stack.length - 1]; - for (tmp = 0; (0 <= _c ? tmp < _c : tmp > _c); (0 <= _c ? tmp += 1 : tmp -= 1)) { - __this.tokens.splice(i, 0, ['CALL_END', ')', __this.tokens[i][2]]); - } - size = stack[stack.length - 1] + 1; - stack[stack.length - 1] = 0; - return size; - }; - })(this); - return this.scanTokens((function(__this) { - return function(prev, token, post, i) { - var _c, _d, j, nx, open, size, tag; - tag = token[0]; - if (tag === 'OUTDENT') { - stack[stack.length - 2] += stack.pop(); - } - open = stack[stack.length - 1] > 0; - if (prev && prev.spaced && include(IMPLICIT_FUNC, prev[0]) && include(IMPLICIT_CALL, tag) && !(tag === '!' && (('IN' === (_c = post[0]) || 'OF' === _c)))) { - __this.tokens.splice(i, 0, ['CALL_START', '(', token[2]]); - stack[stack.length - 1] += 1; - if (include(EXPRESSION_START, tag)) { - stack.push(0); - } - return 2; - } + closeCalls = __bind(function(i) { + var _c, size, tmp; + _c = stack[stack.length - 1]; + for (tmp = 0; (0 <= _c ? tmp < _c : tmp > _c); (0 <= _c ? tmp += 1 : tmp -= 1)) { + this.tokens.splice(i, 0, ['CALL_END', ')', this.tokens[i][2]]); + } + size = stack[stack.length - 1] + 1; + stack[stack.length - 1] = 0; + return size; + }, this); + return this.scanTokens(__bind(function(prev, token, post, i) { + var _c, _d, j, nx, open, size, tag; + tag = token[0]; + if (tag === 'OUTDENT') { + stack[stack.length - 2] += stack.pop(); + } + open = stack[stack.length - 1] > 0; + if (prev && prev.spaced && include(IMPLICIT_FUNC, prev[0]) && include(IMPLICIT_CALL, tag) && !(tag === '!' && (('IN' === (_c = post[0]) || 'OF' === _c)))) { + this.tokens.splice(i, 0, ['CALL_START', '(', token[2]]); + stack[stack.length - 1] += 1; if (include(EXPRESSION_START, tag)) { - if (tag === 'INDENT' && !token.generated && open && !(prev && include(IMPLICIT_BLOCK, prev[0]))) { - size = closeCalls(i); - stack.push(0); - return size; - } stack.push(0); - return 1; } - if (open && !token.generated && prev[0] !== ',' && (!post || include(IMPLICIT_END, tag))) { - j = 1; - while ((typeof (_d = (nx = __this.tokens[i + j])) !== "undefined" && _d !== null) && include(IMPLICIT_END, nx[0])) { - j++; - } - if ((typeof nx !== "undefined" && nx !== null) && nx[0] === ',' && __this.tokens[i + j - 1][0] === 'OUTDENT') { - if (tag === 'TERMINATOR') { - __this.tokens.splice(i, 1); - } - } else { - size = closeCalls(i); - if (tag !== 'OUTDENT' && include(EXPRESSION_END, tag)) { - stack.pop(); - } - return size; - } - } - if (tag !== 'OUTDENT' && include(EXPRESSION_END, tag)) { - stack[stack.length - 2] += stack.pop(); - return 1; + return 2; + } + if (include(EXPRESSION_START, tag)) { + if (tag === 'INDENT' && !token.generated && open && !(prev && include(IMPLICIT_BLOCK, prev[0]))) { + size = closeCalls(i); + stack.push(0); + return size; } + stack.push(0); return 1; - }; - })(this)); + } + if (open && !token.generated && prev[0] !== ',' && (!post || include(IMPLICIT_END, tag))) { + j = 1; + while ((typeof (_d = (nx = this.tokens[i + j])) !== "undefined" && _d !== null) && include(IMPLICIT_END, nx[0])) { + j++; + } + if ((typeof nx !== "undefined" && nx !== null) && nx[0] === ',' && this.tokens[i + j - 1][0] === 'OUTDENT') { + if (tag === 'TERMINATOR') { + this.tokens.splice(i, 1); + } + } else { + size = closeCalls(i); + if (tag !== 'OUTDENT' && include(EXPRESSION_END, tag)) { + stack.pop(); + } + return size; + } + } + if (tag !== 'OUTDENT' && include(EXPRESSION_END, tag)) { + stack[stack.length - 2] += stack.pop(); + return 1; + } + return 1; + }, this)); }; Rewriter.prototype.addImplicitIndentation = function() { - return this.scanTokens((function(__this) { - return function(prev, token, post, i) { - var _c, idx, indent, insertion, outdent, parens, pre, starter, tok; - if (token[0] === 'ELSE' && prev[0] !== 'OUTDENT') { - __this.tokens.splice.apply(__this.tokens, [i, 0].concat(__this.indentation(token))); - return 2; + return this.scanTokens(__bind(function(prev, token, post, i) { + var _c, idx, indent, insertion, outdent, parens, pre, starter, tok; + if (token[0] === 'ELSE' && prev[0] !== 'OUTDENT') { + this.tokens.splice.apply(this.tokens, [i, 0].concat(this.indentation(token))); + return 2; + } + if (token[0] === 'CATCH' && (this.tokens[i + 2][0] === 'TERMINATOR' || this.tokens[i + 2][0] === 'FINALLY')) { + this.tokens.splice.apply(this.tokens, [i + 2, 0].concat(this.indentation(token))); + return 4; + } + if (!(include(SINGLE_LINERS, token[0]) && post[0] !== 'INDENT' && !(token[0] === 'ELSE' && post[0] === 'IF'))) { + return 1; + } + starter = token[0]; + _c = this.indentation(token); + indent = _c[0]; + outdent = _c[1]; + indent.generated = (outdent.generated = true); + this.tokens.splice(i + 1, 0, indent); + idx = i + 1; + parens = 0; + while (true) { + idx += 1; + tok = this.tokens[idx]; + pre = this.tokens[idx - 1]; + if ((!tok || (include(SINGLE_CLOSERS, tok[0]) && tok[1] !== ';' && parens === 0) || (tok[0] === ')' && parens === 0)) && !(tok[0] === 'ELSE' && !('IF' === starter || 'THEN' === starter))) { + insertion = pre[0] === "," ? idx - 1 : idx; + this.tokens.splice(insertion, 0, outdent); + break; } - if (token[0] === 'CATCH' && (__this.tokens[i + 2][0] === 'TERMINATOR' || __this.tokens[i + 2][0] === 'FINALLY')) { - __this.tokens.splice.apply(__this.tokens, [i + 2, 0].concat(__this.indentation(token))); - return 4; + if (tok[0] === '(') { + parens += 1; } - if (!(include(SINGLE_LINERS, token[0]) && post[0] !== 'INDENT' && !(token[0] === 'ELSE' && post[0] === 'IF'))) { - return 1; + if (tok[0] === ')') { + parens -= 1; } - starter = token[0]; - _c = __this.indentation(token); - indent = _c[0]; - outdent = _c[1]; - indent.generated = (outdent.generated = true); - __this.tokens.splice(i + 1, 0, indent); - idx = i + 1; - parens = 0; - while (true) { - idx += 1; - tok = __this.tokens[idx]; - pre = __this.tokens[idx - 1]; - if ((!tok || (include(SINGLE_CLOSERS, tok[0]) && tok[1] !== ';' && parens === 0) || (tok[0] === ')' && parens === 0)) && !(tok[0] === 'ELSE' && !('IF' === starter || 'THEN' === starter))) { - insertion = pre[0] === "," ? idx - 1 : idx; - __this.tokens.splice(insertion, 0, outdent); - break; - } - if (tok[0] === '(') { - parens += 1; - } - if (tok[0] === ')') { - parens -= 1; - } - } - if (!(token[0] === 'THEN')) { - return 1; - } - __this.tokens.splice(i, 1); - return 0; - }; - })(this)); + } + if (!(token[0] === 'THEN')) { + return 1; + } + this.tokens.splice(i, 1); + return 0; + }, this)); }; Rewriter.prototype.ensureBalance = function(pairs) { var _c, _d, key, levels, line, open, openLine, unclosed, value; levels = {}; openLine = {}; - this.scanTokens((function(__this) { - return function(prev, token, post, i) { - var _c, _d, _e, _f, close, open, pair; - _d = pairs; - for (_c = 0, _e = _d.length; _c < _e; _c++) { - pair = _d[_c]; - _f = pair; - open = _f[0]; - close = _f[1]; - levels[open] = levels[open] || 0; - if (token[0] === open) { - if (levels[open] === 0) { - openLine[open] = token[2]; - } - levels[open] += 1; - } - if (token[0] === close) { - levels[open] -= 1; - } - if (levels[open] < 0) { - throw new Error(("too many " + (token[1]) + " on line " + (token[2] + 1))); + this.scanTokens(__bind(function(prev, token, post, i) { + var _c, _d, _e, _f, close, open, pair; + _d = pairs; + for (_c = 0, _e = _d.length; _c < _e; _c++) { + pair = _d[_c]; + _f = pair; + open = _f[0]; + close = _f[1]; + levels[open] = levels[open] || 0; + if (token[0] === open) { + if (levels[open] === 0) { + openLine[open] = token[2]; } + levels[open] += 1; } - return 1; - }; - })(this)); + if (token[0] === close) { + levels[open] -= 1; + } + if (levels[open] < 0) { + throw new Error(("too many " + (token[1]) + " on line " + (token[2] + 1))); + } + } + return 1; + }, this)); unclosed = (function() { _c = []; _d = levels; for (key in _d) { @@ -282,41 +270,39 @@ val = _c[key]; (debt[key] = 0); } - return this.scanTokens((function(__this) { - return function(prev, token, post, i) { - var inv, match, mtag, oppos, tag; - tag = token[0]; - inv = INVERSES[token[0]]; - if (include(EXPRESSION_START, tag)) { - stack.push(token); - return 1; - } else if (include(EXPRESSION_END, tag)) { - if (debt[inv] > 0) { - debt[inv] -= 1; - __this.tokens.splice(i, 1); - return 0; - } else { - match = stack.pop(); - mtag = match[0]; - oppos = INVERSES[mtag]; - if (tag === oppos) { - return 1; - } - debt[mtag] += 1; - val = [oppos, mtag === 'INDENT' ? match[1] : oppos]; - if ((__this.tokens[i + 2] == undefined ? undefined : __this.tokens[i + 2][0]) === mtag) { - __this.tokens.splice(i + 3, 0, val); - stack.push(match); - } else { - __this.tokens.splice(i, 0, val); - } + return this.scanTokens(__bind(function(prev, token, post, i) { + var inv, match, mtag, oppos, tag; + tag = token[0]; + inv = INVERSES[token[0]]; + if (include(EXPRESSION_START, tag)) { + stack.push(token); + return 1; + } else if (include(EXPRESSION_END, tag)) { + if (debt[inv] > 0) { + debt[inv] -= 1; + this.tokens.splice(i, 1); + return 0; + } else { + match = stack.pop(); + mtag = match[0]; + oppos = INVERSES[mtag]; + if (tag === oppos) { return 1; } - } else { + debt[mtag] += 1; + val = [oppos, mtag === 'INDENT' ? match[1] : oppos]; + if ((this.tokens[i + 2] == undefined ? undefined : this.tokens[i + 2][0]) === mtag) { + this.tokens.splice(i + 3, 0, val); + stack.push(match); + } else { + this.tokens.splice(i, 0, val); + } return 1; } - }; - })(this)); + } else { + return 1; + } + }, this)); }; Rewriter.prototype.indentation = function(token) { return [['INDENT', 2, token[2]], ['OUTDENT', 2, token[2]]]; diff --git a/src/nodes.coffee b/src/nodes.coffee index 56958ced..051e0959 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -185,12 +185,6 @@ exports.Expressions: class Expressions extends BaseNode @expressions[idx]: last.makeReturn() this - # A bound function uses a local `__this` variable instead of the real `this`. - rewriteThis: -> - @traverseChildren false, (child) -> - if child instanceof ValueNode and child.base.value is 'this' - child.base: literal '__this' - # An **Expressions** is the only node that can serve as the root. compile: (o) -> o: or {} @@ -866,7 +860,7 @@ exports.CodeNode: class CodeNode extends BaseNode top: del o, 'top' o.scope: sharedScope or new Scope(o.scope, @body, this) o.top: true - o.indent: @idt(if @bound then 2 else 1) + o.indent: @idt(1) del o, 'noWrap' del o, 'globals' i: 0 @@ -886,13 +880,11 @@ exports.CodeNode: class CodeNode extends BaseNode i: + 1 params: (param.compile(o) for param in params) @body.makeReturn() - @body.rewriteThis() if @bound (o.scope.parameter(param)) for param in params code: if @body.expressions.length then "\n${ @body.compileWithDeclarations(o) }\n" else '' - func: "function(${ params.join(', ') }) {$code${ code and @idt(if @bound then 1 else 0) }}" - func: "($func)" if top and not @bound - return func unless @bound - "(function(__this) {\n${@idt(1)}return $func;\n$@tab})(this)" + func: "function(${ params.join(', ') }) {$code${ code and @tab }}" + return "${utility('bind')}($func, this)" if @bound + if top then "($func)" else func topSensitive: -> true @@ -1496,6 +1488,13 @@ UTILITIES: { } """ + # Create a function bound to the current value of "this". + bind: """ + function(func, context) { + return function(){ return func.apply(context, arguments); }; + } + """ + # Shortcuts to speed up the lookup time for native functions. hasProp: 'Object.prototype.hasOwnProperty' slice: 'Array.prototype.slice' diff --git a/test/test_functions.coffee b/test/test_functions.coffee index c115fe6f..d1ea17ee 100644 --- a/test/test_functions.coffee +++ b/test/test_functions.coffee @@ -19,17 +19,27 @@ ok y.x() is 3 obj: { - name: "Fred" + name: 'Fred' bound: -> - (=> ok(this.name is "Fred"))() + (=> ok(this.name is 'Fred'))() unbound: -> (-> ok(!this.name?))() + + nested: -> + (=> + (=> + (=> + ok this.name is 'Fred' + )() + )() + )() } obj.unbound() obj.bound() +obj.nested() # Python decorator style wrapper that memoizes any function