diff --git a/lib/coffee_script/nodes.js b/lib/coffee_script/nodes.js index aef4cad1..050abd3a 100644 --- a/lib/coffee_script/nodes.js +++ b/lib/coffee_script/nodes.js @@ -1,4 +1,13 @@ (function(){ + var compact, dup, flatten; + var __hasProp = Object.prototype.hasOwnProperty; + // 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" + // method, which wraps "compile_node" in some extra smarts, to know when the + // generated code should be wrapped up in a closure. An options hash is passed + // and cloned throughout, containing messages from higher in the AST, + // information about the current scope, and indentation level. exports.Node = function Node() { var __a; var arguments = Array.prototype.slice.call(arguments, 0); @@ -191,4 +200,244 @@ exports.Expressions.wrap = function wrap(values) { return this.values = values; }; + // Some helper functions + // TODO -- shallow (1 deep) flatten.. + // need recursive version.. + flatten = function flatten(aggList, newList) { + var __a, __b, item; + __a = newList; + for (__b = 0; __b < __a.length; __b++) { + item = __a[__b]; + aggList.push(item); + } + return aggList; + }; + compact = function compact(input) { + var __a, __b, __c, compected, item; + compected = []; + __a = []; __b = input; + for (__c = 0; __c < __b.length; __c++) { + item = __b[__c]; + __a.push((typeof item !== "undefined" && item !== null) ? compacted.push(item) : null); + } + return __a; + }; + 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; + }; + exports.Node.prototype.TAB = ' '; + // Tag this node as a statement, meaning that it can't be used directly as + // the result of an expression. + exports.Node.prototype.mark_as_statement = function mark_as_statement() { + return this.is_statement = function is_statement() { + return true; + }; + }; + // Tag this node as a statement that cannot be transformed into an expression. + // (break, continue, etc.) It doesn't make sense to try to transform it. + exports.Node.prototype.mark_as_statement_only = function mark_as_statement_only() { + this.mark_as_statement(); + return this.is_statement_only = function is_statement_only() { + return true; + }; + }; + // This node needs to know if it's being compiled as a top-level statement, + // in order to compile without special expression conversion. + exports.Node.prototype.mark_as_top_sensitive = function mark_as_top_sensitive() { + return this.is_top_sensitive = function is_top_sensitive() { + return true; + }; + }; + // Provide a quick implementation of a children method. + exports.Node.prototype.children = function children(attributes) { + var __a, __b, agg, compacted, item; + // TODO -- are these optimal impls of flatten and compact + // .. do better ones exist in a stdlib? + agg = []; + __a = attributes; + for (__b = 0; __b < __a.length; __b++) { + item = __a[__b]; + agg = flatten(agg, item); + } + compacted = compact(agg); + return this.children = function children() { + return compacted; + }; + }; + exports.Node.prototype.write = function write(code) { + // hm.. + // TODO -- should print to STDOUT in "VERBOSE" how to + // go about this.. ? jsonify 'this'? + // use node's puts ?? + return code; + }; + // This is extremely important -- we convert JS statements into expressions + // by wrapping them in a closure, only if it's possible, and we're not at + // the top level of a block (which would be unnecessary), and we haven't + // already been asked to return the result. + exports.Node.prototype.compile = function compile(o) { + var closure, opts, top; + // TODO -- need JS dup/clone + opts = (typeof !o !== "undefined" && !o !== null) ? { + } : o; + this.options = opts; + this.indent = opts.indent; + top = this.options.top; + !this.is_top_sentitive() ? (this.options.top = undefined) : null; + closure = this.is_statement() && !this.is_statement_only() && !top && typeof (this) === "CommentNode"; + closure = closure && !this.do_i_contain(function(n) { + return n.is_statement_only(); + }); + return closure ? this.compile_closure(this.options) : compile_node(this.options); + }; + // Statements converted into expressions share scope with their parent + // closure, to preserve JavaScript-style lexical scope. + exports.Node.prototype.compile_closure = function compile_closure(o) { + var opts; + opts = (typeof !o !== "undefined" && !o !== null) ? { + } : o; + this.indent = opts.indent; + opts.shared_scope = o.scope; + return exports.ClosureNode.wrap(this).compile(opts); + }; + // Quick short method for the current indentation level, plus tabbing in. + exports.Node.prototype.idt = function idt(tLvl) { + var __a, __b, __c, __d, tabAmt, tabs, x; + tabs = (typeof tLvl !== "undefined" && tLvl !== null) ? tLvl : 0; + tabAmt = ''; + __c = 0; __d = tabs; + for (__b=0, x=__c; (__c <= __d ? x < __d : x > __d); (__c <= __d ? x += 1 : x -= 1), __b++) { + tabAmt = tabAmt + this.TAB; + } + return this.indent + tabAmt; + }; + //Does this node, or any of it's children, contain a node of a certain kind? + exports.Node.prototype.do_i_contain = function do_i_contain(block) { + var __a, __b, node; + __a = this.children; + for (__b = 0; __b < __a.length; __b++) { + node = __a[__b]; + if (block(node)) { + return true; + } + if (node instanceof exports.Node && node.do_i_contain(block)) { + return true; + } + } + return false; + }; + // Default implementations of the common node methods. + exports.Node.prototype.unwrap = function unwrap() { + return this; + }; + exports.Node.prototype.children = []; + exports.Node.prototype.is_a_statement = function is_a_statement() { + return false; + }; + exports.Node.prototype.is_a_statement_only = function is_a_statement_only() { + return false; + }; + exports.Node.prototype.is_top_sensitive = function is_top_sensitive() { + return false; + }; + // A collection of nodes, each one representing an expression. + // exports.Expressions: (nodes) -> + // this.mark_as_statement() + // this.expressions: [] + // this.children([this.expressions]) + // for n in nodes + // this.expressions: flatten this.expressions, n + // exports.Expressions extends exports.Node + exports.Expressions.prototype.TRAILING_WHITESPACE = /\s+$/; + // Wrap up a node as an Expressions, unless it already is. + exports.Expressions.prototype.wrap = function wrap(nodes) { + if (nodes.length === 1 && nodes[0] instanceof exports.Expressions) { + return nodes[0]; + } + return new Expressions(nodes); + }; + // Tack an expression on to the end of this expression list. + exports.Expressions.prototype.push = function push(node) { + this.expressions.push(node); + return this; + }; + // Tack an expression on to the beginning of this expression list. + exports.Expressions.prototype.unshift = function unshift(node) { + this.expressions.unshift(node); + return this; + }; + // If this Expressions consists of a single node, pull it back out. + exports.Expressions.prototype.unwrap = function unwrap() { + return this.expressions.length === 1 ? this.expressions[0] : this; + }; + // Is this an empty block of code? + exports.Expressions.prototype.is_empty = function is_empty() { + return this.expressions.length === 0; + }; + // Is the node last in this block of expressions. + exports.Expressions.prototype.is_last = function is_last(node) { + var arr_length; + arr_length = this.expressions.length; + this.last_index = this.last_index || this.expressions[arr_length - 1] instanceof exports.CommentNode ? -2 : -1; + return node === this.expressions[arr_length - this.last_index]; + }; + exports.Expressions.prototype.compile = function compile(o) { + var opts; + opts = (typeof o !== "undefined" && o !== null) ? o : { + }; + return opts.scope ? exports.Expressions.__superClass__.compile.call(this, dup(opts)) : this.compile_root(o); + }; + // Compile each expression in the Expressions body. + exports.Expressions.prototype.compile_node = function compile_node(options) { + var __a, __b, __c, __d, __e, code, compiled, e, line, opts; + opts = (typeof options !== "undefined" && options !== null) ? options : { + }; + compiled = []; + __a = this.expressions; + for (__b = 0; __b < __a.length; __b++) { + e = __a[__b]; + compiled.push(this.compile_expression(e, dup(options))); + } + code = ''; + __c = []; __d = compiled; + for (__e = 0; __e < __d.length; __e++) { + line = __d[__e]; + __c.push((code = code + line + '\n')); + } + return __c; + }; + // If this is the top-level Expressions, wrap everything in a safety closure. + exports.Expressions.prototype.compile_root = function compile_root(o) { + var code, indent, opts; + opts = (typeof o !== "undefined" && o !== null) ? o : { + }; + indent = opts.no_wrap ? '' : this.TAB; + this.indent = indent; + opts.indent = indent; + opts.scope = new Scope(null, this, null); + code = opts.globals ? compile_node(opts) : compile_with_declarations(opts); + code.replace(this.TRAILING_WHITESPACE, ''); + return this.write(opts.no_wrap ? code : "(function(){\n" + code + "\n})();"); + }; })(); \ No newline at end of file diff --git a/lib/coffee_script/parser.js b/lib/coffee_script/parser.js index 2a9fe562..3ad533ca 100644 --- a/lib/coffee_script/parser.js +++ b/lib/coffee_script/parser.js @@ -192,22 +192,29 @@ return new SplatNode(yytext); }) ], - // # A regular splat. - // Splat: [ - // o "Expression . . .", -> new SplatNode($1) - // ] + // A regular splat. + Splat: [o("Expression . . .", function() { + return new SplatNode($1); + }) + ], // Expressions that can be treated as values. Value: [o("IDENTIFIER", function() { return new ValueNode(yytext); }), o("Literal", function() { return new ValueNode($1); + }), o("Array", function() { + return new ValueNode($1); + }), o("Object", function() { + return new ValueNode($1); + }), o("Parenthetical", function() { + return new ValueNode($1); + }), o("Range", function() { + return new ValueNode($1); }), - // o "Array", -> new ValueNode($1) - // o "Object", -> new ValueNode($1) - // o "Parenthetical", -> new ValueNode($1) - // o "Range", -> new ValueNode($1) // o "Value Accessor", -> $1.push($2) - // o "Invocation Accessor", -> new ValueNode($1, [$2]) + o("Invocation Accessor", function() { + return new ValueNode($1, [$2]); + }) ] // # Accessing into an object or array, through dot or index notation. // Accessor: [ diff --git a/lib/coffee_script/scope.js b/lib/coffee_script/scope.js new file mode 100644 index 00000000..5e85cbbd --- /dev/null +++ b/lib/coffee_script/scope.js @@ -0,0 +1,73 @@ +(function(){ + var dup; + 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 + // 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) { + 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.variables = { + }; + __a = this.temp_variable = this.parent ? dup(this.parent.temp_variable) : '__a'; + 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; + found = this.check(name); + if (found || remote) { + return found; + } + this.variables[name] = 'var'; + return found; + }; + // Define a local variable as originating from a parameter in current scope + // -- no var required. + exports.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)) { + 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; + }; +})(); \ No newline at end of file diff --git a/src/nodes.coffee b/src/nodes.coffee index 82bf2456..bae86a4e 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1,4 +1,11 @@ -exports.Node: -> @values: arguments; @name: this.constructor.name +# 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" +# method, which wraps "compile_node" in some extra smarts, to know when the +# generated code should be wrapped up in a closure. An options hash is passed +# and cloned throughout, containing messages from higher in the AST, +# information about the current scope, and indentation level. +exports.Node : -> @values: arguments; @name: this.constructor.name exports.Expressions : -> @name: this.constructor.name; @values: arguments exports.LiteralNode : -> @name: this.constructor.name; @values: arguments @@ -28,3 +35,180 @@ exports.ParentheticalNode : -> @name: this.constructor.name; @values: arguments exports.IfNode : -> @name: this.constructor.name; @values: arguments exports.Expressions.wrap : (values) -> @values: values + + +# Some helper functions + +# TODO -- shallow (1 deep) flatten.. +# need recursive version.. +flatten: (aggList, newList) -> + for item in newList + aggList.push(item) + aggList + +compact: (input) -> + compected: [] + for item in input + if item? + compacted.push(item) + +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 + +exports.Node::TAB: ' ' + +# Tag this node as a statement, meaning that it can't be used directly as +# the result of an expression. +exports.Node::mark_as_statement: -> + this.is_statement: -> true + +# Tag this node as a statement that cannot be transformed into an expression. +# (break, continue, etc.) It doesn't make sense to try to transform it. +exports.Node::mark_as_statement_only: -> + this.mark_as_statement() + this.is_statement_only: -> true + +# This node needs to know if it's being compiled as a top-level statement, +# in order to compile without special expression conversion. +exports.Node::mark_as_top_sensitive: -> + this.is_top_sensitive: -> true + +# Provide a quick implementation of a children method. +exports.Node::children: (attributes) -> + # TODO -- are these optimal impls of flatten and compact + # .. do better ones exist in a stdlib? + agg: [] + for item in attributes + agg: flatten agg, item + compacted: compact agg + this.children: -> + compacted + +exports.Node::write: (code) -> + # hm.. + # TODO -- should print to STDOUT in "VERBOSE" how to + # go about this.. ? jsonify 'this'? + # use node's puts ?? + code + +# This is extremely important -- we convert JS statements into expressions +# by wrapping them in a closure, only if it's possible, and we're not at +# the top level of a block (which would be unnecessary), and we haven't +# already been asked to return the result. +exports.Node::compile: (o) -> + # TODO -- need JS dup/clone + opts: if not o? then {} else o + this.options: opts + this.indent: opts.indent + top: this.options.top + if not this.is_top_sentitive() + this.options.top: undefined + closure: this.is_statement() and not this.is_statement_only() and not top and typeof(this) == "CommentNode" + closure &&= not this.do_i_contain (n) -> n.is_statement_only() + if closure then this.compile_closure(this.options) else compile_node(this.options) + +# Statements converted into expressions share scope with their parent +# closure, to preserve JavaScript-style lexical scope. +exports.Node::compile_closure: (o) -> + opts: if not o? then {} else o + this.indent: opts.indent + opts.shared_scope: o.scope + exports.ClosureNode.wrap(this).compile(opts) + +# Quick short method for the current indentation level, plus tabbing in. +exports.Node::idt: (tLvl) -> + tabs: if tLvl? then tLvl else 0 + tabAmt: '' + for x in [0...tabs] + tabAmt: tabAmt + this.TAB + this.indent + tabAmt + +#Does this node, or any of it's children, contain a node of a certain kind? +exports.Node::do_i_contain: (block) -> + for node in this.children + return true if block(node) + return true if node instanceof exports.Node and node.do_i_contain(block) + false + +# Default implementations of the common node methods. +exports.Node::unwrap: -> this +exports.Node::children: [] +exports.Node::is_a_statement: -> false +exports.Node::is_a_statement_only: -> false +exports.Node::is_top_sensitive: -> false + +# A collection of nodes, each one representing an expression. +# exports.Expressions: (nodes) -> +# this.mark_as_statement() +# this.expressions: [] +# this.children([this.expressions]) +# for n in nodes +# this.expressions: flatten this.expressions, n +# exports.Expressions extends exports.Node + +exports.Expressions::TRAILING_WHITESPACE: /\s+$/ + +# Wrap up a node as an Expressions, unless it already is. +exports.Expressions::wrap: (nodes) -> + return nodes[0] if nodes.length == 1 and nodes[0] instanceof exports.Expressions + new Expressions(nodes) + +# Tack an expression on to the end of this expression list. +exports.Expressions::push: (node) -> + this.expressions.push(node) + this + +# Tack an expression on to the beginning of this expression list. +exports.Expressions::unshift: (node) -> + this.expressions.unshift(node) + this + +# If this Expressions consists of a single node, pull it back out. +exports.Expressions::unwrap: -> + if this.expressions.length == 1 then this.expressions[0] else this + +# Is this an empty block of code? +exports.Expressions::is_empty: -> + this.expressions.length == 0 + +# Is the node last in this block of expressions. +exports.Expressions::is_last: (node) -> + arr_length: this.expressions.length + this.last_index ||= if this.expressions[arr_length - 1] instanceof exports.CommentNode then -2 else -1 + node == this.expressions[arr_length - this.last_index] + +exports.Expressions::compile: (o) -> + opts: if o? then o else {} + if opts.scope then super(dup(opts)) else this.compile_root(o) + +# Compile each expression in the Expressions body. +exports.Expressions::compile_node: (options) -> + opts: if options? then options else {} + compiled: [] + for e in this.expressions + compiled.push(this.compile_expression(e, dup(options))) + code: '' + for line in compiled + code: code + line + '\n' + +# If this is the top-level Expressions, wrap everything in a safety closure. +exports.Expressions::compile_root: (o) -> + opts: if o? then o else {} + indent: if opts.no_wrap then '' else this.TAB + this.indent: indent + opts.indent: indent + opts.scope: new Scope(null, this, null) + code: if opts.globals then compile_node(opts) else compile_with_declarations(opts) + code.replace(this.TRAILING_WHITESPACE, '') + this.write(if opts.no_wrap then code else "(function(){\n"+code+"\n})();") + diff --git a/src/parser.coffee b/src/parser.coffee index 83568539..bc33c519 100644 --- a/src/parser.coffee +++ b/src/parser.coffee @@ -216,21 +216,21 @@ grammar: { o "PARAM . . .", -> new SplatNode(yytext) ] - # # A regular splat. - # Splat: [ - # o "Expression . . .", -> new SplatNode($1) - # ] + # A regular splat. + Splat: [ + o "Expression . . .", -> new SplatNode($1) + ] # Expressions that can be treated as values. Value: [ o "IDENTIFIER", -> new ValueNode(yytext) o "Literal", -> new ValueNode($1) - # o "Array", -> new ValueNode($1) - # o "Object", -> new ValueNode($1) - # o "Parenthetical", -> new ValueNode($1) - # o "Range", -> new ValueNode($1) + o "Array", -> new ValueNode($1) + o "Object", -> new ValueNode($1) + o "Parenthetical", -> new ValueNode($1) + o "Range", -> new ValueNode($1) # o "Value Accessor", -> $1.push($2) - # o "Invocation Accessor", -> new ValueNode($1, [$2]) + o "Invocation Accessor", -> new ValueNode($1, [$2]) ] # # Accessing into an object or array, through dot or index notation. diff --git a/src/scope.coffee b/src/scope.coffee new file mode 100644 index 00000000..d0cf38eb --- /dev/null +++ b/src/scope.coffee @@ -0,0 +1,49 @@ +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 +# 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' + +# 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' + found + +# Define a local variable as originating from a parameter in current scope +# -- no var required. +exports.Scope::parameter: (name) -> + this.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)) + +# You can reset a found variable on the immediate scope. +exports.Scope::reset: (name) -> + this.variables[name]: undefined