From 0b5b6113eeb2226d0823259b360e54dc77dafb14 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 8 Feb 2010 22:22:59 -0500 Subject: [PATCH] CoffeeScript-in-CoffeeScript just had it's first self-compiled snippet. --- lib/coffee_script/lexer.js | 4 +- lib/coffee_script/nodes.js | 141 +++++++++++++++++++++++++++----- lib/coffee_script/nodes.rb | 148 +++++++++++++++++----------------- lib/coffee_script/rewriter.js | 4 +- src/lexer.coffee | 4 +- src/nodes.coffee | 116 +++++++++++++++++++++++--- src/rewriter.coffee | 4 +- 7 files changed, 312 insertions(+), 109 deletions(-) diff --git a/lib/coffee_script/lexer.js b/lib/coffee_script/lexer.js index 527bf5b6..720d56f1 100644 --- a/lib/coffee_script/lexer.js +++ b/lib/coffee_script/lexer.js @@ -16,7 +16,7 @@ JS = /^(``|`([\s\S]*?)([^\\]|\\\\)`)/; OPERATOR = /^([+\*&|\/\-%=<>:!?]+)/; WHITESPACE = /^([ \t]+)/; - COMMENT = /^(((\n?[ \t]*)?#.*$)+)/; + COMMENT = /^(((\n?[ \t]*)?#[^\n]*)+)/; CODE = /^((-|=)>)/; REGEX = /^(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/; MULTI_DENT = /^((\n([ \t]*))+)(\.)?/; @@ -184,7 +184,7 @@ if (!((comment = this.match(COMMENT, 1)))) { return false; } - this.line += comment.match(MULTILINER).length; + this.line += (comment.match(MULTILINER) || []).length; this.token('COMMENT', comment.replace(COMMENT_CLEANER, '').split(MULTILINER)); this.token('TERMINATOR', "\n"); this.i += comment.length; diff --git a/lib/coffee_script/nodes.js b/lib/coffee_script/nodes.js index 5b5d9cf7..e805953a 100644 --- a/lib/coffee_script/nodes.js +++ b/lib/coffee_script/nodes.js @@ -1,5 +1,5 @@ (function(){ - var Expressions, LiteralNode, Node, TAB, TRAILING_WHITESPACE, compact, del, dup, flatten, inherit, merge, statement; + var CommentNode, Expressions, LiteralNode, Node, ReturnNode, TAB, TRAILING_WHITESPACE, ValueNode, compact, del, dup, flatten, inherit, merge, statement; var __hasProp = Object.prototype.hasOwnProperty; process.mixin(require('./scope')); // The abstract base class for all CoffeeScript nodes. @@ -86,6 +86,13 @@ __a = this.values = arguments; return SliceNode === this.constructor ? this : __a; }; + exports.ThisNode = function ThisNode() { + var __a; + var arguments = Array.prototype.slice.call(arguments, 0); + this.name = this.constructor.name; + __a = this.values = arguments; + return ThisNode === this.constructor ? this : __a; + }; exports.AssignNode = function AssignNode() { var __a; var arguments = Array.prototype.slice.call(arguments, 0); @@ -200,18 +207,18 @@ TRAILING_WHITESPACE = /\s+$/g; // Flatten nested arrays recursively. flatten = function flatten(list) { - var __a, __b, __c, item, memo; + var __a, __b, item, memo; memo = []; - __a = []; __b = list; - for (__c = 0; __c < __b.length; __c++) { - item = __b[__c]; + __a = list; + for (__b = 0; __b < __a.length; __b++) { + item = __a[__b]; if (item instanceof Array) { return memo.concat(flatten(item)); } memo.push(item); memo; } - return __a; + return memo; }; // Remove all null values from an array. compact = function compact(input) { @@ -273,8 +280,7 @@ // Quickie inheritance convenience wrapper to reduce typing. inherit = function inherit(parent, props) { var __a, __b, klass, name, prop; - klass = props.constructor; - delete props.constructor; + klass = del(props, 'constructor'); __a = function(){}; __a.prototype = parent.prototype; klass.__superClass__ = parent.prototype; @@ -299,11 +305,11 @@ klass.prototype.is_statement = function is_statement() { return true; }; - return klass.prototype.is_statement_only = function is_statement_only() { - if (only) { + if (only) { + return ((klass.prototype.is_statement_only = function is_statement_only() { return true; - } - }; + })); + } }; // The abstract base class for all CoffeeScript nodes. // All nodes are implement a "compile_node" method, which performs the @@ -322,7 +328,7 @@ this.options = dup(o || { }); this.indent = o.indent; - top = this.top_sensitive() ? o.top : del(obj('top')); + top = this.top_sensitive() ? o.top : del(o, 'top'); closure = this.is_statement() && !this.is_statement_only() && !top && !o.returns && !this instanceof CommentNode && !this.contains(function(node) { return node.is_statement_only(); }); @@ -338,7 +344,7 @@ // Quick short method for the current indentation level, plus tabbing in. Node.prototype.idt = function idt(tabs) { var __a, __b, __c, __d, i, idt; - idt = this.indent; + idt = (this.indent || ''); __c = 0; __d = (tabs || 0); for (__b=0, i=__c; (__c <= __d ? i <= __d : i >= __d); (__c <= __d ? i += 1 : i -= 1), __b++) { idt += TAB; @@ -436,7 +442,7 @@ var args, argv, code; code = this.compile_node(o); args = this.contains(function(node) { - return node instanceof ValueNode && node.arguments(); + return node instanceof ValueNode && node.is_arguments(); }); argv = args && o.scope.check('arguments') ? '' : 'var '; if (args) { @@ -456,8 +462,7 @@ this.indent = o.indent; stmt = node.is_statement(); // We need to return the result if this is the last node in the expressions body. - returns = o.returns && this.is_last(node) && !node.is_statement_only(); - delete o.returns; + returns = del(o, 'returns') && this.is_last(node) && !node.is_statement_only(); // Return the regular compile of the node, unless we need to return the result. if (!(returns)) { return (stmt ? '' : this.idt()) + node.compile(merge(o, { @@ -492,7 +497,8 @@ LiteralNode = (exports.LiteralNode = inherit(Node, { constructor: function constructor(value) { this.value = value; - return this.children = [value]; + this.children = [value]; + return this; }, // Break and continue must be treated as statements -- they lose their meaning // when wrapped in a closure. @@ -507,4 +513,103 @@ } })); LiteralNode.prototype.is_statement_only = LiteralNode.prototype.is_statement; + // Return an expression, or wrap it in a closure and return it. + ReturnNode = (exports.ReturnNode = inherit(Node, { + constructor: function constructor(expression) { + this.expression = expression; + this.children = [expression]; + return this; + }, + compile_node: function compile_node(o) { + if (this.expression.is_statement()) { + return this.expression.compile(merge(o, { + returns: true + })); + } + return this.idt() + 'return ' + this.expression.compile(o) + ';'; + } + })); + statement(ReturnNode, true); + // A value, indexed or dotted into, or vanilla. + ValueNode = (exports.ValueNode = inherit(Node, { + SOAK: " == undefined ? undefined : ", + constructor: function constructor(base, properties) { + this.base = base; + this.properties = flatten(properties || []); + this.children = flatten(this.base, this.properties); + return this; + }, + push: function push(prop) { + this.properties.push(prop); + return this.children.push(prop); + }, + has_properties: function has_properties() { + return this.properties.length || this.base instanceof ThisNode; + }, + is_array: function is_array() { + return this.base instanceof ArrayNode && !this.has_properties(); + }, + is_object: function is_object() { + return this.base instanceof ObjectNode && !this.has_properties(); + }, + is_splice: function is_splice() { + return this.has_properties() && this.properties[this.properties.length - 1] instanceof SliceNode; + }, + is_arguments: function is_arguments() { + return this.base === 'arguments'; + }, + unwrap: function unwrap() { + return this.properties.length ? this : this.base; + }, + // Values are statements if their base is a statement. + is_statement: function is_statement() { + return this.base.is_statement && this.base.is_statement() && !this.has_properties(); + }, + compile_node: function compile_node(o) { + var __a, __b, baseline, code, only, part, parts, prop, props, soaked, temp; + soaked = false; + only = del(o, 'only_first'); + props = only ? this.properties.slice(0, this.properties.length) : this.properties; + baseline = this.base.compile(o); + parts = [baseline]; + __a = props; + for (__b = 0; __b < __a.length; __b++) { + prop = __a[__b]; + if (prop instanceof AccessorNode && prop.soak) { + soaked = true; + if (this.base instanceof CallNode && prop === props[0]) { + temp = o.scope.free_variable(); + parts[parts.length - 1] = '(' + temp + ' = ' + baseline + ')' + this.SOAK + ((baseline = temp + prop.compile(o))); + } else { + parts[parts.length - 1] = this.SOAK + (baseline += prop.compile(o)); + } + } else { + part = prop.compile(o); + baseline += part; + parts.push(part); + } + } + this.last = parts[parts.length - 1]; + this.source = parts.length > 1 ? parts.slice(0, parts.length).join('') : null; + code = parts.join('').replace(/\)\(\)\)/, '()))'); + if (!(soaked)) { + return code; + } + return '(' + code + ')'; + } + })); + // Pass through CoffeeScript comments into JavaScript comments at the + // same position. + CommentNode = (exports.CommentNode = inherit(Node, { + constructor: function constructor(lines) { + this.lines = lines; + return this; + }, + compile_node: function compile_node(o) { + var delimiter; + delimiter = "\n" + this.idt() + '//'; + return delimiter + this.lines.join(delimiter); + } + })); + statement(CommentNode); })(); \ No newline at end of file diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index ee416006..90da709d 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -226,6 +226,80 @@ module CoffeeScript end end + # A value, indexed or dotted into, or vanilla. + class ValueNode < Node + children :base, :properties + attr_reader :last, :source + + # Soak up undefined properties and call attempts. + SOAK = " == undefined ? undefined : " + + def initialize(base, properties=[]) + @base, @properties = base, [properties].flatten + end + + def <<(other) + @properties << other + self + end + + def properties? + return !@properties.empty? || @base.is_a?(ThisNode) + end + + def array? + @base.is_a?(ArrayNode) && !properties? + end + + def object? + @base.is_a?(ObjectNode) && !properties? + end + + def splice? + properties? && @properties.last.is_a?(SliceNode) + end + + def arguments? + @base.to_s == 'arguments' + end + + def unwrap + @properties.empty? ? @base : self + end + + # Values are statements if their base is a statement. + def statement? + @base.is_a?(Node) && @base.statement? && !properties? + end + + def compile_node(o) + soaked = false + only = o.delete(:only_first) + props = only ? @properties[0...-1] : @properties + baseline = @base.compile(o) + parts = [baseline.dup] + props.each do |prop| + if prop.is_a?(AccessorNode) && prop.soak + soaked = true + if @base.is_a?(CallNode) && prop == props.first + temp = o[:scope].free_variable + parts[-1] = "(#{temp} = #{baseline})#{SOAK}#{baseline = temp.to_s + prop.compile(o)}" + else + parts[-1] << "#{SOAK}#{baseline += prop.compile(o)}" + end + else + part = prop.compile(o) + baseline += part + parts << part + end + end + @last = parts.last + @source = parts.length > 1 ? parts[0...-1].join('') : nil + code = parts.join('').gsub(')())', '()))') + write(soaked ? "(#{code})" : code) + end + end + # Pass through CoffeeScript comments into JavaScript comments at the # same position. class CommentNode < Node @@ -324,80 +398,6 @@ module CoffeeScript end - # A value, indexed or dotted into, or vanilla. - class ValueNode < Node - children :base, :properties - attr_reader :last, :source - - # Soak up undefined properties and call attempts. - SOAK = " == undefined ? undefined : " - - def initialize(base, properties=[]) - @base, @properties = base, [properties].flatten - end - - def <<(other) - @properties << other - self - end - - def properties? - return !@properties.empty? || @base.is_a?(ThisNode) - end - - def array? - @base.is_a?(ArrayNode) && !properties? - end - - def object? - @base.is_a?(ObjectNode) && !properties? - end - - def splice? - properties? && @properties.last.is_a?(SliceNode) - end - - def arguments? - @base.to_s == 'arguments' - end - - def unwrap - @properties.empty? ? @base : self - end - - # Values are statements if their base is a statement. - def statement? - @base.is_a?(Node) && @base.statement? && !properties? - end - - def compile_node(o) - soaked = false - only = o.delete(:only_first) - props = only ? @properties[0...-1] : @properties - baseline = @base.compile(o) - parts = [baseline.dup] - props.each do |prop| - if prop.is_a?(AccessorNode) && prop.soak - soaked = true - if @base.is_a?(CallNode) && prop == props.first - temp = o[:scope].free_variable - parts[-1] = "(#{temp} = #{baseline})#{SOAK}#{baseline = temp.to_s + prop.compile(o)}" - else - parts[-1] << "#{SOAK}#{baseline += prop.compile(o)}" - end - else - part = prop.compile(o) - baseline += part - parts << part - end - end - @last = parts.last - @source = parts.length > 1 ? parts[0...-1].join('') : nil - code = parts.join('').gsub(')())', '()))') - write(soaked ? "(#{code})" : code) - end - end - # A dotted accessor into a part of a value, or the :: shorthand for # an accessor into the object's prototype. class AccessorNode < Node diff --git a/lib/coffee_script/rewriter.js b/lib/coffee_script/rewriter.js index 5e1a3d90..82261c7f 100644 --- a/lib/coffee_script/rewriter.js +++ b/lib/coffee_script/rewriter.js @@ -91,11 +91,11 @@ this.tokens.splice(i + 2, 1); this.tokens.splice(i - 2, 1); return 0; - } else if (prev[0] === 'TERMINATOR' && after[0] === 'INDENT') { + } else if (prev && prev[0] === 'TERMINATOR' && after[0] === 'INDENT') { this.tokens.splice(i + 2, 1); this.tokens[i - 1] = after; return 1; - } else if (prev[0] !== 'TERMINATOR' && prev[0] !== 'INDENT' && prev[0] !== 'OUTDENT') { + } else if (prev && prev[0] !== 'TERMINATOR' && prev[0] !== 'INDENT' && prev[0] !== 'OUTDENT') { this.tokens.splice(i, 0, ['TERMINATOR', "\n", prev[2]]); return 2; } else { diff --git a/src/lexer.coffee b/src/lexer.coffee index 1431b8f9..086c60c9 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -29,7 +29,7 @@ HEREDOC : /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?( JS : /^(``|`([\s\S]*?)([^\\]|\\\\)`)/ OPERATOR : /^([+\*&|\/\-%=<>:!?]+)/ WHITESPACE : /^([ \t]+)/ -COMMENT : /^(((\n?[ \t]*)?#.*$)+)/ +COMMENT : /^(((\n?[ \t]*)?#[^\n]*)+)/ CODE : /^((-|=)>)/ REGEX : /^(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/ MULTI_DENT : /^((\n([ \t]*))+)(\.)?/ @@ -153,7 +153,7 @@ lex::regex_token: -> # Matches and conumes comments. lex::comment_token: -> return false unless comment: this.match COMMENT, 1 - this.line += comment.match(MULTILINER).length + this.line += (comment.match(MULTILINER) or []).length this.token 'COMMENT', comment.replace(COMMENT_CLEANER, '').split(MULTILINER) this.token 'TERMINATOR', "\n" this.i += comment.length diff --git a/src/nodes.coffee b/src/nodes.coffee index abe7ddb3..58e363a3 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -19,6 +19,7 @@ exports.AccessorNode : -> @name: this.constructor.name; @values: arguments exports.IndexNode : -> @name: this.constructor.name; @values: arguments exports.RangeNode : -> @name: this.constructor.name; @values: arguments exports.SliceNode : -> @name: this.constructor.name; @values: arguments +exports.ThisNode : -> @name: this.constructor.name; @values: arguments exports.AssignNode : -> @name: this.constructor.name; @values: arguments exports.OpNode : -> @name: this.constructor.name; @values: arguments exports.CodeNode : -> @name: this.constructor.name; @values: arguments @@ -51,6 +52,7 @@ flatten: (list) -> return memo.concat(flatten(item)) if item instanceof Array memo.push(item) memo + memo # Remove all null values from an array. compact: (input) -> @@ -78,8 +80,7 @@ del: (obj, key) -> # Quickie inheritance convenience wrapper to reduce typing. inherit: (parent, props) -> - klass: props.constructor - delete props.constructor + klass: del(props, 'constructor') klass extends parent (klass.prototype[name]: prop) for name, prop of props klass @@ -93,7 +94,7 @@ inherit: (parent, props) -> # Mark a node as a statement, or a statement only. statement: (klass, only) -> klass::is_statement: -> true - klass::is_statement_only: -> true if only + (klass::is_statement_only: -> true) if only # The abstract base class for all CoffeeScript nodes. @@ -112,7 +113,7 @@ Node: exports.Node: -> Node::compile: (o) -> @options: dup(o || {}) @indent: o.indent - top: if @top_sensitive() then o.top else del obj 'top' + top: if @top_sensitive() then o.top else del o, 'top' closure: @is_statement() and not @is_statement_only() and not top and not o.returns and not this instanceof CommentNode and not @contains (node) -> node.is_statement_only() @@ -127,7 +128,7 @@ Node::compile_closure: (o) -> # Quick short method for the current indentation level, plus tabbing in. Node::idt: (tabs) -> - idt: @indent + idt: (@indent || '') idt += TAB for i in [0..(tabs or 0)] idt @@ -197,7 +198,7 @@ Expressions: exports.Expressions: inherit Node, { # pushed up to the top. compile_with_declarations: (o) -> code: @compile_node(o) - args: @contains (node) -> node instanceof ValueNode and node.arguments() + args: @contains (node) -> node instanceof ValueNode and node.is_arguments() argv: if args and o.scope.check('arguments') then '' else 'var ' code: @idt() + argv + "arguments = Array.prototype.slice.call(arguments, 0);\n" + code if args code: @idt() + 'var ' + o.scope.compiled_assignments() + ";\n" + code if o.scope.has_assignments(this) @@ -209,8 +210,7 @@ Expressions: exports.Expressions: inherit Node, { @indent: o.indent stmt: node.is_statement() # We need to return the result if this is the last node in the expressions body. - returns: o.returns and @is_last(node) and not node.is_statement_only() - delete o.returns + returns: del(o, 'returns') and @is_last(node) and not node.is_statement_only() # Return the regular compile of the node, unless we need to return the result. return (if stmt then '' else @idt()) + node.compile(merge(o, {top: true})) + (if stmt then '' else ';') unless returns # If it's a statement, the node knows how to return itself. @@ -237,6 +237,7 @@ LiteralNode: exports.LiteralNode: inherit Node, { constructor: (value) -> @value: value @children: [value] + this # Break and continue must be treated as statements -- they lose their meaning # when wrapped in a closure. @@ -253,7 +254,104 @@ LiteralNode: exports.LiteralNode: inherit Node, { LiteralNode::is_statement_only: LiteralNode::is_statement - +# Return an expression, or wrap it in a closure and return it. +ReturnNode: exports.ReturnNode: inherit Node, { + + constructor: (expression) -> + @expression: expression + @children: [expression] + this + + compile_node: (o) -> + return @expression.compile(merge(o, {returns: true})) if @expression.is_statement() + @idt() + 'return ' + @expression.compile(o) + ';' + +} + +statement ReturnNode, true + + +# A value, indexed or dotted into, or vanilla. +ValueNode: exports.ValueNode: inherit Node, { + + SOAK: " == undefined ? undefined : " + + constructor: (base, properties) -> + @base: base + @properties: flatten(properties or []) + @children: flatten(@base, @properties) + this + + push: (prop) -> + @properties.push(prop) + @children.push(prop) + + has_properties: -> + @properties.length or @base instanceof ThisNode + + is_array: -> + @base instanceof ArrayNode and not @has_properties() + + is_object: -> + @base instanceof ObjectNode and not @has_properties() + + is_splice: -> + @has_properties() and @properties[@properties.length - 1] instanceof SliceNode + + is_arguments: -> + @base is 'arguments' + + unwrap: -> + if @properties.length then this else @base + + # Values are statements if their base is a statement. + is_statement: -> + @base.is_statement and @base.is_statement() and not @has_properties() + + compile_node: (o) -> + soaked: false + only: del(o, 'only_first') + props: if only then @properties[0...@properties.length] else @properties + baseline: @base.compile o + parts: [baseline] + + for prop in props + if prop instanceof AccessorNode and prop.soak + soaked: true + if @base instanceof CallNode and prop is props[0] + temp: o.scope.free_variable() + parts[parts.length - 1]: '(' + temp + ' = ' + baseline + ')' + @SOAK + (baseline: temp + prop.compile(o)) + else + parts[parts.length - 1]: @SOAK + (baseline += prop.compile(o)) + else + part: prop.compile(o) + baseline += part + parts.push(part) + + @last: parts[parts.length - 1] + @source: if parts.length > 1 then parts[0...parts.length].join('') else null + code: parts.join('').replace(/\)\(\)\)/, '()))') + return code unless soaked + '(' + code + ')' + +} + + +# Pass through CoffeeScript comments into JavaScript comments at the +# same position. +CommentNode: exports.CommentNode: inherit Node, { + + constructor: (lines) -> + @lines: lines + this + + compile_node: (o) -> + delimiter: "\n" + @idt() + '//' + delimiter + @lines.join(delimiter) + +} + +statement CommentNode diff --git a/src/rewriter.coffee b/src/rewriter.coffee index cb764731..c27a5bb7 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -77,11 +77,11 @@ re::adjust_comments: -> this.tokens.splice(i + 2, 1) this.tokens.splice(i - 2, 1) return 0 - else if prev[0] is 'TERMINATOR' and after[0] is 'INDENT' + else if prev and prev[0] is 'TERMINATOR' and after[0] is 'INDENT' this.tokens.splice(i + 2, 1) this.tokens[i - 1]: after return 1 - else if prev[0] isnt 'TERMINATOR' and prev[0] isnt 'INDENT' and prev[0] isnt 'OUTDENT' + else if prev and prev[0] isnt 'TERMINATOR' and prev[0] isnt 'INDENT' and prev[0] isnt 'OUTDENT' this.tokens.splice(i, 0, ['TERMINATOR', "\n", prev[2]]) return 2 else