diff --git a/lib/coffee_script/nodes.js b/lib/coffee_script/nodes.js index afbcb639..5b5d9cf7 100644 --- a/lib/coffee_script/nodes.js +++ b/lib/coffee_script/nodes.js @@ -1,6 +1,7 @@ (function(){ var Expressions, LiteralNode, Node, TAB, TRAILING_WHITESPACE, compact, del, dup, flatten, inherit, merge, statement; var __hasProp = Object.prototype.hasOwnProperty; + process.mixin(require('./scope')); // The abstract base class for all CoffeeScript nodes. // All nodes are implement a "compile_node" method, which performs the // code generation for that node. To compile a node, call the "compile" @@ -271,7 +272,7 @@ }; // Quickie inheritance convenience wrapper to reduce typing. inherit = function inherit(parent, props) { - var __a, __b, __c, klass, name, prop; + var __a, __b, klass, name, prop; klass = props.constructor; delete props.constructor; __a = function(){}; @@ -279,16 +280,13 @@ klass.__superClass__ = parent.prototype; klass.prototype = new __a(); klass.prototype.constructor = klass; - klass.prototype[name] = (function() { - __b = []; __c = props; - for (name in __c) { - prop = __c[name]; - if (__hasProp.call(__c, name)) { - __b.push(prop); - } + __b = props; + for (name in __b) { + prop = __b[name]; + if (__hasProp.call(__b, name)) { + ((klass.prototype[name] = prop)); } - return __b; - }).call(this); + } return klass; }; // # Provide a quick implementation of a children method. @@ -378,20 +376,10 @@ }; // A collection of nodes, each one representing an expression. Expressions = (exports.Expressions = inherit(Node, { - constructor: function constructor() { - var nodes; - nodes = Array.prototype.slice.call(arguments, 0); + constructor: function constructor(nodes) { this.expressions = flatten(nodes); - return this.children = this.expressions; - }, - // Wrap up a node as an Expressions, unless it already is. - wrap: function wrap() { - var nodes; - nodes = Array.prototype.slice.call(arguments, 0); - if (nodes.length === 1 && nodes[0] instanceof Expressions) { - return nodes[0]; - } - return new Expressions.apply(this, nodes); + this.children = this.expressions; + return this; }, // Tack an expression on to the end of this expression list. push: function push(node) { @@ -483,28 +471,40 @@ })); } // If it's not part of a constructor, we can just return the value of the expression. - if (!((o.scope.function == undefined ? undefined : o.scope.function.is_constructor()))) { + if (!((o.scope.method == undefined ? undefined : o.scope.method.is_constructor()))) { return this.idt() + 'return ' + node.compile(o); } // It's the last line of a constructor, add a safety check. temp = o.scope.free_variable(); - return this.idt() + temp + ' = ' + node.compile(o) + ";\n" + this.idt() + "return " + o.scope.function.name + ' === this.constructor ? this : ' + temp + ';'; + return this.idt() + temp + ' = ' + node.compile(o) + ";\n" + this.idt() + "return " + o.scope.method.name + ' === this.constructor ? this : ' + temp + ';'; } })); + // Wrap up a node as an Expressions, unless it already is one. + Expressions.wrap = function wrap(nodes) { + if (nodes.length === 1 && nodes[0] instanceof Expressions) { + return nodes[0]; + } + return new Expressions(nodes); + }; statement(Expressions); // Literals are static values that can be passed through directly into // JavaScript without translation, eg.: strings, numbers, true, false, null... - LiteralNode = (exports.LiteralNode = function LiteralNode(value) { - var __a; - this.value = value; - __a = this.children = [value]; - return LiteralNode === this.constructor ? this : __a; - }); - // Break and continue must be treated as statements -- they lose their meaning - // when wrapped in a closure. - LiteralNode.prototype.is_statement = function is_statement() { - return this.value === 'break' || this.value === 'continue'; - }; + LiteralNode = (exports.LiteralNode = inherit(Node, { + constructor: function constructor(value) { + this.value = value; + return this.children = [value]; + }, + // Break and continue must be treated as statements -- they lose their meaning + // when wrapped in a closure. + is_statement: function is_statement() { + return this.value === 'break' || this.value === 'continue'; + }, + compile_node: function compile_node(o) { + var end, idt; + idt = this.is_statement() ? this.idt() : ''; + end = this.is_statement() ? ';' : ''; + return idt + this.value + end; + } + })); LiteralNode.prototype.is_statement_only = LiteralNode.prototype.is_statement; - LiteralNode.prototype.compile_node = function compile_node(o) { }; })(); \ No newline at end of file diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index ce8e5e9e..29f2e0e8 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -180,8 +180,8 @@ module CoffeeScript end - # Literals are static values that have a Ruby representation, eg.: a string, a number, - # true, false, nil, etc. + # Literals are static values that can be passed through directly into + # JavaScript without translation, eg.: strings, numbers, true, false, null... class LiteralNode < Node children :value diff --git a/lib/coffee_script/parser.js b/lib/coffee_script/parser.js index 1eeb7dd1..842c0f0d 100644 --- a/lib/coffee_script/parser.js +++ b/lib/coffee_script/parser.js @@ -362,7 +362,7 @@ }), o("WHILE Expression", function() { return new WhileNode($2, null); }), o("Expression WHILE Expression", function() { - return new WhileNode($3, Expressions.wrap($1)); + return new WhileNode($3, Expressions.wrap([$1])); }) ], // Array comprehensions, including guard and current index. @@ -460,11 +460,11 @@ If: [o("IfBlock IfEnd", function() { return $1.add_else($2); }), o("Expression IF Expression", function() { - return new IfNode($3, Expressions.wrap($1), null, { + return new IfNode($3, Expressions.wrap([$1]), null, { statement: true }); }), o("Expression UNLESS Expression", function() { - return new IfNode($3, Expressions.wrap($1), null, { + return new IfNode($3, Expressions.wrap([$1]), null, { statement: true, invert: true }); diff --git a/lib/coffee_script/scope.js b/lib/coffee_script/scope.js index 5e85cbbd..172afbd4 100644 --- a/lib/coffee_script/scope.js +++ b/lib/coffee_script/scope.js @@ -1,50 +1,27 @@ (function(){ - var dup; + var Scope, succ; var __hasProp = Object.prototype.hasOwnProperty; - dup = function dup(input) { - var __a, __b, __c, key, output, val; - output = null; - if (input instanceof Array) { - output = []; - __a = input; - for (__b = 0; __b < __a.length; __b++) { - val = __a[__b]; - output.push(val); - } - } else { - output = { - }; - __c = input; - for (key in __c) { - val = __c[key]; - if (__hasProp.call(__c, key)) { - output.key = val; - } - } - output; - } - return output; - }; - // scope objects form a tree corresponding to the shape of the function + // Scope objects form a tree corresponding to the shape of the function // definitions present in the script. They provide lexical scope, to determine // whether a variable has been seen before or if it needs to be declared. - exports.Scope = function Scope(parent, expressions, func) { + // + // Initialize a scope with its parent, for lookups up the chain, + // as well as the Expressions body where it should declare its variables, + // and the function that it wraps. + Scope = (exports.Scope = function Scope(parent, expressions, method) { var __a; - // Initialize a scope with its parent, for lookups up the chain, - // as well as the Expressions body where it should declare its variables, - // and the function that it wraps. this.parent = parent; this.expressions = expressions; - this.function = func; + this.method = method; this.variables = { }; - __a = this.temp_variable = this.parent ? dup(this.parent.temp_variable) : '__a'; + this.temp_variable = this.parent ? this.parent.temp_variable : '__a'; + __a = this; return Scope === this.constructor ? this : __a; - }; + }); // Look up a variable in lexical scope, or declare it if not found. - exports.Scope.prototype.find = function find(name, rem) { - var found, remote; - remote = (typeof rem !== "undefined" && rem !== null) ? rem : false; + Scope.prototype.find = function find(name, remote) { + var found; found = this.check(name); if (found || remote) { return found; @@ -54,20 +31,99 @@ }; // Define a local variable as originating from a parameter in current scope // -- no var required. - exports.Scope.prototype.parameter = function parameter(name) { + Scope.prototype.parameter = function parameter(name) { return this.variables[name] = 'param'; }; // Just check to see if a variable has already been declared. - exports.Scope.prototype.check = function check(name) { - if ((typeof this.variables[name] !== "undefined" && this.variables[name] !== null)) { + Scope.prototype.check = function check(name) { + if (this.variables[name]) { return true; } - // TODO: what does that ruby !! mean..? need to follow up - // .. this next line is prolly wrong .. return !!(this.parent && this.parent.check(name)); }; // You can reset a found variable on the immediate scope. - exports.Scope.prototype.reset = function reset(name) { - return this.variables[name] = undefined; + Scope.prototype.reset = function reset(name) { + return delete this.variables[name]; + }; + // Find an available, short, name for a compiler-generated variable. + Scope.prototype.free_variable = function free_variable() { + while (check(this.temp_variable)) { + ((this.temp_variable = succ(this.temp_variable))); + } + this.variables[this.temp_variable] = 'var'; + return this.temp_variable; + }; + // Ensure that an assignment is made at the top of scope (or top-level + // scope, if requested). + Scope.prototype.assign = function assign(name, value, top_level) { + if (top_level && this.parent) { + return this.parent.assign(name, value, top_level); + } + return this.variables[name] = { + value: value, + assigned: true + }; + }; + // Does this scope reference any variables that need to be declared in the + // given function body? + Scope.prototype.has_declarations = function has_declarations(body) { + return body === this.expressions && this.declared_variables().length; + }; + // Does this scope reference any assignments that need to be declared at the + // top of the given function body? + Scope.prototype.has_assignments = function has_assignments(body) { + return body === this.expressions && this.assigned_variables().length; + }; + // Return the list of variables first declared in current scope. + Scope.prototype.declared_variables = function declared_variables() { + var __a, __b, key, val; + return ((function() { + __a = []; __b = this.variables; + for (key in __b) { + val = __b[key]; + if (__hasProp.call(__b, key)) { + if (val === 'var') { + __a.push(key); + } + } + } + return __a; + }).call(this)).sort(); + }; + // Return the list of variables that are supposed to be assigned at the top + // of scope. + Scope.prototype.assigned_variables = function assigned_variables() { + var __a, __b, key, val; + return ((function() { + __a = []; __b = this.variables; + for (key in __b) { + val = __b[key]; + if (__hasProp.call(__b, key)) { + if (val.assigned) { + __a.push([key, val.value]); + } + } + } + return __a; + }).call(this)).sort(); + }; + Scope.prototype.compiled_declarations = function compiled_declarations() { + return this.declared_variables().join(', '); + }; + Scope.prototype.compiled_assignments = function compiled_assignments() { + var __a, __b, __c, t; + return ((function() { + __a = []; __b = this.assigned_variables(); + for (__c = 0; __c < __b.length; __c++) { + t = __b[__c]; + __a.push(t[0] + ' = ' + t[1]); + } + return __a; + }).call(this)).join(', '); + }; + // Helper functions: + // The next character alphabetically, to produce the following string. + succ = function succ(str) { + return str.slice(0, str.length - 1) + String.fromCharCode(str.charCodeAt(str.length - 1) + 1); }; })(); \ No newline at end of file diff --git a/lib/coffee_script/scope.rb b/lib/coffee_script/scope.rb index 9463afa7..fe4385bc 100644 --- a/lib/coffee_script/scope.rb +++ b/lib/coffee_script/scope.rb @@ -55,10 +55,14 @@ module CoffeeScript @variables[name.to_sym] = Value.new(value) end + # Does this scope reference any variables that need to be declared in the + # given function body? def declarations?(body) !declared_variables.empty? && body == @expressions end + # Does this scope reference any assignments that need to be declared at the + # top of the given function body? def assignments?(body) !assigned_variables.empty? && body == @expressions end diff --git a/src/nodes.coffee b/src/nodes.coffee index 65565221..abe7ddb3 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1,3 +1,5 @@ +process.mixin require './scope' + # The abstract base class for all CoffeeScript nodes. # All nodes are implement a "compile_node" method, which performs the # code generation for that node. To compile a node, call the "compile" @@ -79,7 +81,7 @@ inherit: (parent, props) -> klass: props.constructor delete props.constructor klass extends parent - klass.prototype[name]: prop for name, prop of props + (klass.prototype[name]: prop) for name, prop of props klass # # Provide a quick implementation of a children method. @@ -147,14 +149,10 @@ Node::top_sensitive: -> false # A collection of nodes, each one representing an expression. Expressions: exports.Expressions: inherit Node, { - constructor: (nodes...) -> + constructor: (nodes) -> @expressions: flatten nodes @children: @expressions - - # Wrap up a node as an Expressions, unless it already is. - wrap: (nodes...) -> - return nodes[0] if nodes.length is 1 and nodes[0] instanceof Expressions - new Expressions(nodes...) + this # Tack an expression on to the end of this expression list. push: (node) -> @@ -218,30 +216,42 @@ Expressions: exports.Expressions: inherit Node, { # If it's a statement, the node knows how to return itself. return node.compile(merge(o, {returns: true})) if node.is_statement() # If it's not part of a constructor, we can just return the value of the expression. - return @idt() + 'return ' + node.compile(o) unless o.scope.function?.is_constructor() + return @idt() + 'return ' + node.compile(o) unless o.scope.method?.is_constructor() # It's the last line of a constructor, add a safety check. temp: o.scope.free_variable() - @idt() + temp + ' = ' + node.compile(o) + ";\n" + @idt() + "return " + o.scope.function.name + ' === this.constructor ? this : ' + temp + ';' + @idt() + temp + ' = ' + node.compile(o) + ";\n" + @idt() + "return " + o.scope.method.name + ' === this.constructor ? this : ' + temp + ';' } +# Wrap up a node as an Expressions, unless it already is one. +Expressions.wrap: (nodes) -> + return nodes[0] if nodes.length is 1 and nodes[0] instanceof Expressions + new Expressions(nodes) + statement Expressions # Literals are static values that can be passed through directly into # JavaScript without translation, eg.: strings, numbers, true, false, null... -LiteralNode: exports.LiteralNode: (value) -> - @value: value - @children: [value] +LiteralNode: exports.LiteralNode: inherit Node, { -# Break and continue must be treated as statements -- they lose their meaning -# when wrapped in a closure. -LiteralNode::is_statement: -> - @value is 'break' or @value is 'continue' + constructor: (value) -> + @value: value + @children: [value] + + # Break and continue must be treated as statements -- they lose their meaning + # when wrapped in a closure. + is_statement: -> + @value is 'break' or @value is 'continue' + + compile_node: (o) -> + idt: if @is_statement() then @idt() else '' + end: if @is_statement() then ';' else '' + idt + @value + end + +} LiteralNode::is_statement_only: LiteralNode::is_statement -LiteralNode::compile_node: (o) -> - diff --git a/src/parser.coffee b/src/parser.coffee index 6e4ae3e6..7a339fd9 100644 --- a/src/parser.coffee +++ b/src/parser.coffee @@ -353,7 +353,7 @@ grammar: { While: [ o "WHILE Expression Block", -> new WhileNode($2, $3) o "WHILE Expression", -> new WhileNode($2, null) - o "Expression WHILE Expression", -> new WhileNode($3, Expressions.wrap($1)) + o "Expression WHILE Expression", -> new WhileNode($3, Expressions.wrap([$1])) ] # Array comprehensions, including guard and current index. @@ -427,8 +427,8 @@ grammar: { # The full complement of if blocks, including postfix one-liner ifs and unlesses. If: [ o "IfBlock IfEnd", -> $1.add_else($2) - o "Expression IF Expression", -> new IfNode($3, Expressions.wrap($1), null, {statement: true}) - o "Expression UNLESS Expression", -> new IfNode($3, Expressions.wrap($1), null, {statement: true, invert: true}) + o "Expression IF Expression", -> new IfNode($3, Expressions.wrap([$1]), null, {statement: true}) + o "Expression UNLESS Expression", -> new IfNode($3, Expressions.wrap([$1]), null, {statement: true, invert: true}) ] } diff --git a/src/scope.coffee b/src/scope.coffee index d0cf38eb..09cc550d 100644 --- a/src/scope.coffee +++ b/src/scope.coffee @@ -1,49 +1,80 @@ -dup: (input) -> - output: null - if input instanceof Array - output: [] - for val in input - output.push(val) - else - output: {} - for key, val of input - output.key: val - output - output - -# scope objects form a tree corresponding to the shape of the function +# Scope objects form a tree corresponding to the shape of the function # definitions present in the script. They provide lexical scope, to determine # whether a variable has been seen before or if it needs to be declared. -exports.Scope: (parent, expressions, func) -> - # Initialize a scope with its parent, for lookups up the chain, - # as well as the Expressions body where it should declare its variables, - # and the function that it wraps. - this.parent: parent - this.expressions: expressions - this.function: func - this.variables: {} - this.temp_variable: if this.parent then dup(this.parent.temp_variable) else '__a' +# +# Initialize a scope with its parent, for lookups up the chain, +# as well as the Expressions body where it should declare its variables, +# and the function that it wraps. +Scope: exports.Scope: (parent, expressions, method) -> + @parent: parent + @expressions: expressions + @method: method + @variables: {} + @temp_variable: if @parent then @parent.temp_variable else '__a' + this # Look up a variable in lexical scope, or declare it if not found. -exports.Scope::find: (name, rem) -> - remote: if rem? then rem else false - found: this.check(name) - return found if found || remote - this.variables[name]: 'var' +Scope::find: (name, remote) -> + found: @check name + return found if found or remote + @variables[name]: 'var' found # Define a local variable as originating from a parameter in current scope # -- no var required. -exports.Scope::parameter: (name) -> - this.variables[name]: 'param' +Scope::parameter: (name) -> + @variables[name]: 'param' # Just check to see if a variable has already been declared. -exports.Scope::check: (name) -> - return true if this.variables[name]? - # TODO: what does that ruby !! mean..? need to follow up - # .. this next line is prolly wrong .. - not not (this.parent and this.parent.check(name)) +Scope::check: (name) -> + return true if @variables[name] + !!(@parent and @parent.check(name)) # You can reset a found variable on the immediate scope. -exports.Scope::reset: (name) -> - this.variables[name]: undefined +Scope::reset: (name) -> + delete @variables[name] + +# Find an available, short, name for a compiler-generated variable. +Scope::free_variable: -> + (@temp_variable: succ(@temp_variable)) while check @temp_variable + @variables[@temp_variable]: 'var' + @temp_variable + +# Ensure that an assignment is made at the top of scope (or top-level +# scope, if requested). +Scope::assign: (name, value, top_level) -> + return @parent.assign(name, value, top_level) if top_level and @parent + @variables[name]: {value: value, assigned: true} + +# Does this scope reference any variables that need to be declared in the +# given function body? +Scope::has_declarations: (body) -> + body is @expressions and @declared_variables().length + +# Does this scope reference any assignments that need to be declared at the +# top of the given function body? +Scope::has_assignments: (body) -> + body is @expressions and @assigned_variables().length + +# Return the list of variables first declared in current scope. +Scope::declared_variables: -> + (key for key, val of @variables when val is 'var').sort() + +# Return the list of variables that are supposed to be assigned at the top +# of scope. +Scope::assigned_variables: -> + ([key, val.value] for key, val of @variables when val.assigned).sort() + +Scope::compiled_declarations: -> + @declared_variables().join(', ') + +Scope::compiled_assignments: -> + (t[0] + ' = ' + t[1] for t in @assigned_variables()).join(', ') + + +# Helper functions: + +# The next character alphabetically, to produce the following string. +succ: (str) -> + str.slice(0, str.length - 1) + + String.fromCharCode(str.charCodeAt(str.length - 1) + 1)