From 001c915c21040f0046590353e06e80f2f2be7136 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 9 Feb 2010 20:53:25 -0500 Subject: [PATCH] Self-compiler: object literals. --- lib/coffee_script/lexer.js | 2 +- lib/coffee_script/nodes.js | 169 +++++++++++++++++++++++++++++++----- lib/coffee_script/nodes.rb | 168 +++++++++++++++++------------------ lib/coffee_script/parser.js | 62 +++++++------ src/lexer.coffee | 2 +- src/nodes.coffee | 128 ++++++++++++++++++++++----- src/parser.coffee | 47 +++++----- 7 files changed, 403 insertions(+), 175 deletions(-) diff --git a/lib/coffee_script/lexer.js b/lib/coffee_script/lexer.js index 48885f27..99a16654 100644 --- a/lib/coffee_script/lexer.js +++ b/lib/coffee_script/lexer.js @@ -8,7 +8,7 @@ // Constants ============================================================ // The list of keywords passed verbatim to the parser. KEYWORDS = ["if", "else", "then", "unless", "true", "false", "yes", "no", "on", "off", "and", "or", "is", "isnt", "not", "new", "return", "arguments", "try", "catch", "finally", "throw", "break", "continue", "for", "in", "of", "by", "where", "while", "delete", "instanceof", "typeof", "switch", "when", "super", "extends"]; - // Token matching regexes. + // Token matching regexes. (keep the IDENTIFIER regex in sync with AssignNode.) IDENTIFIER = /^([a-zA-Z$_](\w|\$)*)/; NUMBER = /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i; STRING = /^(""|''|"([\s\S]*?)([^\\]|\\\\)"|'([\s\S]*?)([^\\]|\\\\)')/; diff --git a/lib/coffee_script/nodes.js b/lib/coffee_script/nodes.js index 94b997ab..3e2435ef 100644 --- a/lib/coffee_script/nodes.js +++ b/lib/coffee_script/nodes.js @@ -1,5 +1,5 @@ (function(){ - var AccessorNode, CallNode, CommentNode, Expressions, ExtendsNode, IndexNode, LiteralNode, Node, RangeNode, ReturnNode, SliceNode, TAB, TRAILING_WHITESPACE, ThisNode, ValueNode, any, compact, del, dup, flatten, inherit, merge, statement; + var AccessorNode, AssignNode, CallNode, CommentNode, Expressions, ExtendsNode, IndexNode, LiteralNode, Node, ObjectNode, RangeNode, ReturnNode, SliceNode, TAB, TRAILING_WHITESPACE, ThisNode, ValueNode, any, compact, del, dup, flatten, inherit, merge, statement; var __hasProp = Object.prototype.hasOwnProperty; process.mixin(require('./scope')); // The abstract base class for all CoffeeScript nodes. @@ -341,8 +341,7 @@ // A collection of nodes, each one representing an expression. Expressions = (exports.Expressions = inherit(Node, { constructor: function constructor(nodes) { - this.expressions = flatten(nodes); - this.children = this.expressions; + this.children = (this.expressions = flatten(nodes)); return this; }, // Tack an expression on to the end of this expression list. @@ -451,8 +450,7 @@ // JavaScript without translation, eg.: strings, numbers, true, false, null... LiteralNode = (exports.LiteralNode = inherit(Node, { constructor: function constructor(value) { - this.value = value; - this.children = [value]; + this.children = [(this.value = value)]; return this; }, // Break and continue must be treated as statements -- they lose their meaning @@ -471,8 +469,7 @@ // 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]; + this.children = [(this.expression = expression)]; return this; }, compile_node: function compile_node(o) { @@ -489,9 +486,7 @@ 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); + this.children = flatten((this.base = base), (this.properties = (properties || []))); return this; }, push: function push(prop) { @@ -572,9 +567,7 @@ // calls against the prototype's function of the same name. CallNode = (exports.CallNode = inherit(Node, { constructor: function constructor(variable, args) { - this.variable = variable; - this.args = args || []; - this.children = flatten([this.variable, this.args]); + this.children = flatten([(this.variable = variable), (this.args = (args || []))]); this.prefix = ''; return this; }, @@ -648,9 +641,7 @@ // After goog.inherits from the Closure Library. ExtendsNode = (exports.ExtendsNode = inherit(Node, { constructor: function constructor(child, parent) { - this.child = child; - this.parent = parent; - this.children = [child, parent]; + this.children = [(this.child = child), (this.parent = parent)]; return this; }, // Hooking one constructor into another's prototype chain. @@ -667,8 +658,7 @@ // an accessor into the object's prototype. AccessorNode = (exports.AccessorNode = inherit(Node, { constructor: function constructor(name, tag) { - this.name = name; - this.children = [this.name]; + this.children = [(this.name = name)]; this.prototype = tag === 'prototype'; this.soak = tag === 'soak'; return this; @@ -701,9 +691,7 @@ // or to specify a range for list comprehensions. RangeNode = (exports.RangeNode = inherit(Node, { constructor: function constructor(from, to, exclusive) { - this.from = from; - this.to = to; - this.children = [from, to]; + this.children = [(this.from = from), (this.to = to)]; this.exclusive = !!exclusive; return this; }, @@ -754,4 +742,143 @@ return ".slice(" + from + ', ' + to + plus_part + ')'; } })); + // An object literal. + ObjectNode = (exports.ObjectNode = inherit(Node, { + constructor: function constructor(props) { + this.objects = (this.properties = props || []); + return this; + }, + // All the mucking about with commas is to make sure that CommentNodes and + // AssignNodes get interleaved correctly, with no trailing commas or + // commas affixed to comments. TODO: Extract this and add it to ArrayNode. + compile_node: function compile_node(o) { + var __a, __b, __c, __d, __e, i, indent, join, last_noncom, non_comments, prop, props; + o.indent = this.idt(1); + non_comments = (function() { + __a = []; __b = this.properties; + for (__c = 0; __c < __b.length; __c++) { + prop = __b[__c]; + if (!(prop instanceof CommentNode)) { + __a.push(prop); + } + } + return __a; + }).call(this); + last_noncom = non_comments[non_comments.length - 1]; + props = (function() { + __d = []; __e = this.properties; + for (i = 0; i < __e.length; i++) { + prop = __e[i]; + __d.push((function() { + join = ",\n"; + if (prop === last_noncom || prop instanceof CommentNode) { + join = "\n"; + } + if (i === non_comments.length - 1) { + join = ''; + } + indent = prop instanceof CommentNode ? '' : this.idt(1); + return indent + prop.compile(o) + join; + }).call(this)); + } + return __d; + }).call(this); + return '{\n' + props.join('') + '\n' + this.idt() + '}'; + } + })); + // Setting the value of a local variable, or the value of an object property. + AssignNode = (exports.AssignNode = inherit(Node, { + // Keep the identifier regex in sync with the Lexer. + IDENTIFIER: /^([a-zA-Z$_](\w|\$)*)/, + PROTO_ASSIGN: /^(\S+)\.prototype/, + LEADING_DOT: /^\.(prototype\.)?/, + constructor: function constructor(variable, value, context) { + this.children = [(this.variable = variable), (this.value = value)]; + this.context = context; + return this; + }, + top_sensitive: function top_sensitive() { + return true; + }, + is_value: function is_value() { + return this.variable instanceof ValueNode; + }, + is_statement: function is_statement() { + return this.is_value() && (this.variable.is_array() || this.variable.is_object()); + }, + compile_node: function compile_node(o) { + var last, match, name, proto, stmt, top, val; + top = del(o, 'top'); + if (this.is_statement()) { + return this.compile_pattern_match(o); + } + if (this.is_value() && this.variable.is_splice()) { + return this.compile_splice(o); + } + stmt = del(o, 'as_statement'); + name = this.variable.compile(o); + last = this.is_value() ? this.variable.last.replace(this.LEADING_DOT, '') : name; + match = name.match(this.PROTO_ASSIGN); + proto = match && match[1]; + if (this.value instanceof CodeNode) { + if (last.match(this.IDENTIFIER)) { + this.value.name = last; + } + if (proto) { + this.value.proto = proto; + } + } + if (this.context === 'object') { + return name + ': ' + this.value.compile(o); + } + if (!(this.is_value() && this.variable.has_properties())) { + o.scope.find(name); + } + val = name + ' = ' + this.value.compile(o); + if (stmt) { + return this.idt() + val + ';'; + } + if (!top || o.returns) { + val = '(' + val + ')'; + } + if (o.returns) { + val = this.idt() + 'return ' + val; + } + return val; + }, + // Implementation of recursive pattern matching, when assigning array or + // object literals to a value. Peeks at their properties to assign inner names. + // See: http://wiki.ecmascript.org/doku.php?id=harmony:destructuring + compile_pattern_match: function compile_pattern_match(o) { + var __a, __b, access_class, assigns, i, obj, val, val_var; + val_var = o.scope.free_variable(); + assigns = [this.idt() + val_var + ' = ' + this.value.compile(o) + ';']; + o.top = true; + o.as_statement = true; + __a = this.variable.base.objects; + for (i = 0; i < __a.length; i++) { + obj = __a[i]; + if (this.variable.is_object()) { + __b = [obj.value, obj.variable.base]; + obj = __b[0]; + i = __b[1]; + } + access_class = this.variable.is_array() ? IndexNode : AccessorNode; + obj instanceof SplatNode ? (val = new LiteralNode(obj.compile_value(o, val_var, this.variable.base.objects.indexOf(obj)))) : (val = new ValueNode(val_var, [new access_class(new LiteralNode(i))])); + assigns.push(new AssignNode(obj, val).compile(o)); + } + return assigns.join("\n"); + }, + compile_splice: function compile_splice(o) { + var from, name, plus, range, to; + name = this.variable.compile(merge(o, { + only_first: true + })); + range = this.variable.properties.last.range; + plus = range.exclusive ? '' : ' + 1'; + from = range.from.compile(o); + to = range.to.compile(o) + ' - ' + from + plus; + return name + '.splice.apply(' + name + ', [' + from + ', ' + to + '].concat(' + this.value.compile(o) + '))'; + } + })); })(); \ No newline at end of file diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index da680813..3658e219 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -499,6 +499,75 @@ module CoffeeScript end end + # An object literal. + class ObjectNode < Node + children :properties + alias_method :objects, :properties + + def initialize(properties = []) + @properties = properties + end + + # All the mucking about with commas is to make sure that CommentNodes and + # AssignNodes get interleaved correctly, with no trailing commas or + # commas affixed to comments. TODO: Extract this and add it to ArrayNode. + def compile_node(o) + o[:indent] = idt(1) + joins = Hash.new("\n") + non_comments = @properties.select {|p| !p.is_a?(CommentNode) } + non_comments.each {|p| joins[p] = p == non_comments.last ? "\n" : ",\n" } + props = @properties.map { |prop| + join = joins[prop] + join = '' if prop == @properties.last + indent = prop.is_a?(CommentNode) ? '' : idt(1) + "#{indent}#{prop.compile(o)}#{join}" + }.join('') + write("{\n#{props}\n#{idt}}") + end + end + + # An array literal. + class ArrayNode < Node + children :objects + + def initialize(objects=[]) + @objects = objects + end + + def compile_node(o) + o[:indent] = idt(1) + objects = @objects.map { |obj| + code = obj.compile(o) + obj.is_a?(CommentNode) ? "\n#{code}\n#{o[:indent]}" : + obj == @objects.last ? code : "#{code}, " + }.join('') + ending = objects.include?("\n") ? "\n#{idt}]" : ']' + write("[#{objects}#{ending}") + end + end + + # A faux-node that is never created by the grammar, but is used during + # code generation to generate a quick "array.push(value)" tree of nodes. + class PushNode + def self.wrap(array, expressions) + expr = expressions.unwrap + return expressions if expr.statement_only? || expr.contains? {|n| n.statement_only? } + Expressions.wrap(CallNode.new( + ValueNode.new(LiteralNode.new(array), [AccessorNode.new(Value.new('push'))]), + [expr] + )) + end + end + + # A faux-node used to wrap an expressions body in a closure. + class ClosureNode + def self.wrap(expressions, statement=false) + func = ParentheticalNode.new(CodeNode.new([], Expressions.wrap(expressions))) + call = CallNode.new(ValueNode.new(func, AccessorNode.new(Value.new('call'))), [Value.new('this')]) + statement ? Expressions.wrap(call) : call + end + end + # Setting the value of a local variable, or the value of an object property. class AssignNode < Node top_sensitive @@ -511,14 +580,22 @@ module CoffeeScript @variable, @value, @context = variable, value, context end + def value? + @variable.is_a?(ValueNode) + end + + def statement? + value? && (@variable.array? || @variable.object?) + end + def compile_node(o) - top = o.delete(:top) - return compile_pattern_match(o) if statement? - return compile_splice(o) if value? && @variable.splice? - stmt = o.delete(:as_statement) - name = @variable.compile(o) - last = value? ? @variable.last.to_s.sub(LEADING_DOT, '') : name - proto = name[PROTO_ASSIGN, 1] + top = o.delete(:top) + return compile_pattern_match(o) if statement? + return compile_splice(o) if value? && @variable.splice? + stmt = o.delete(:as_statement) + name = @variable.compile(o) + last = value? ? @variable.last.to_s.sub(LEADING_DOT, '') : name + proto = name[PROTO_ASSIGN, 1] if @value.is_a?(CodeNode) @value.name = last if last.match(Lexer::IDENTIFIER) @value.proto = proto if proto @@ -532,14 +609,6 @@ module CoffeeScript write(val) end - def value? - @variable.is_a?(ValueNode) - end - - def statement? - value? && (@variable.array? || @variable.object?) - end - # Implementation of recursive pattern matching, when assigning array or # object literals to a value. Peeks at their properties to assign inner names. # See: http://wiki.ecmascript.org/doku.php?id=harmony:destructuring @@ -710,75 +779,6 @@ module CoffeeScript end - # An object literal. - class ObjectNode < Node - children :properties - alias_method :objects, :properties - - def initialize(properties = []) - @properties = properties - end - - # All the mucking about with commas is to make sure that CommentNodes and - # AssignNodes get interleaved correctly, with no trailing commas or - # commas affixed to comments. TODO: Extract this and add it to ArrayNode. - def compile_node(o) - o[:indent] = idt(1) - joins = Hash.new("\n") - non_comments = @properties.select {|p| !p.is_a?(CommentNode) } - non_comments.each {|p| joins[p] = p == non_comments.last ? "\n" : ",\n" } - props = @properties.map { |prop| - join = joins[prop] - join = '' if prop == @properties.last - indent = prop.is_a?(CommentNode) ? '' : idt(1) - "#{indent}#{prop.compile(o)}#{join}" - }.join('') - write("{\n#{props}\n#{idt}}") - end - end - - # An array literal. - class ArrayNode < Node - children :objects - - def initialize(objects=[]) - @objects = objects - end - - def compile_node(o) - o[:indent] = idt(1) - objects = @objects.map { |obj| - code = obj.compile(o) - obj.is_a?(CommentNode) ? "\n#{code}\n#{o[:indent]}" : - obj == @objects.last ? code : "#{code}, " - }.join('') - ending = objects.include?("\n") ? "\n#{idt}]" : ']' - write("[#{objects}#{ending}") - end - end - - # A faux-node that is never created by the grammar, but is used during - # code generation to generate a quick "array.push(value)" tree of nodes. - class PushNode - def self.wrap(array, expressions) - expr = expressions.unwrap - return expressions if expr.statement_only? || expr.contains? {|n| n.statement_only? } - Expressions.wrap(CallNode.new( - ValueNode.new(LiteralNode.new(array), [AccessorNode.new(Value.new('push'))]), - [expr] - )) - end - end - - # A faux-node used to wrap an expressions body in a closure. - class ClosureNode - def self.wrap(expressions, statement=false) - func = ParentheticalNode.new(CodeNode.new([], Expressions.wrap(expressions))) - call = CallNode.new(ValueNode.new(func, AccessorNode.new(Value.new('call'))), [Value.new('this')]) - statement ? Expressions.wrap(call) : call - end - end - # A while loop, the only sort of low-level loop exposed by CoffeeScript. From # it, all other loops can be manufactured. class WhileNode < Node diff --git a/lib/coffee_script/parser.js b/lib/coffee_script/parser.js index 0872d1fc..d4cf4277 100644 --- a/lib/coffee_script/parser.js +++ b/lib/coffee_script/parser.js @@ -45,11 +45,19 @@ return new Expressions(); }) ], - // All hard-coded values. These can be printed straight to JavaScript. - Literal: [o("NUMBER", function() { + Identifier: [o("IDENTIFIER", function() { + return new LiteralNode(yytext); + }) + ], + AlphaNumeric: [o("NUMBER", function() { return new LiteralNode(yytext); }), o("STRING", function() { return new LiteralNode(yytext); + }) + ], + // All hard-coded values. These can be printed straight to JavaScript. + Literal: [o("AlphaNumeric", function() { + return $1; }), o("JS", function() { return new LiteralNode(yytext); }), o("REGEX", function() { @@ -80,12 +88,10 @@ }) ], // Assignment within an object literal (can be quoted). - AssignObj: [o("IDENTIFIER ASSIGN Expression", function() { - return new AssignNode(new ValueNode(new LiteralNode(yytext)), $3, 'object'); - }), o("STRING ASSIGN Expression", function() { - return new AssignNode(new ValueNode(new LiteralNode(yytext)), $3, 'object'); - }), o("NUMBER ASSIGN Expression", function() { - return new AssignNode(new ValueNode(new LiteralNode(yytext)), $3, 'object'); + AssignObj: [o("Identifier ASSIGN Expression", function() { + return new AssignNode(new ValueNode($1), $3, 'object'); + }), o("AlphaNumeric ASSIGN Expression", function() { + return new AssignNode(new ValueNode($1), $3, 'object'); }), o("Comment") ], // A return statement. @@ -200,8 +206,8 @@ }) ], // Expressions that can be treated as values. - Value: [o("IDENTIFIER", function() { - return new ValueNode(new LiteralNode(yytext)); + Value: [o("Identifier", function() { + return new ValueNode($1); }), o("Literal", function() { return new ValueNode($1); }), o("Array", function() { @@ -221,12 +227,12 @@ }) ], // Accessing into an object or array, through dot or index notation. - Accessor: [o("PROPERTY_ACCESS IDENTIFIER", function() { - return new AccessorNode(new LiteralNode(yytext)); - }), o("PROTOTYPE_ACCESS IDENTIFIER", function() { - return new AccessorNode(new LiteralNode(yytext), 'prototype'); - }), o("SOAK_ACCESS IDENTIFIER", function() { - return new AccessorNode(new LiteralNode(yytext), 'soak'); + Accessor: [o("PROPERTY_ACCESS Identifier", function() { + return new AccessorNode($2); + }), o("PROTOTYPE_ACCESS Identifier", function() { + return new AccessorNode($2, 'prototype'); + }), o("SOAK_ACCESS Identifier", function() { + return new AccessorNode($2, 'soak'); }), o("Index"), o("Slice", function() { return new SliceNode($1); }) @@ -247,11 +253,11 @@ }), o("AssignObj", function() { return [$1]; }), o("AssignList , AssignObj", function() { - return $1.push($3); + return $1.concat([$3]); }), o("AssignList TERMINATOR AssignObj", function() { - return $1.push($3); + return $1.concat([$3]); }), o("AssignList , TERMINATOR AssignObj", function() { - return $1.push($4); + return $1.concat([$4]); }), o("INDENT AssignList OUTDENT", function() { return $2; }) @@ -290,8 +296,8 @@ // This references, either naked or to a property. This: [o("@", function() { return new ThisNode(); - }), o("@ IDENTIFIER", function() { - return new ThisNode(yytext); + }), o("@ Identifier", function() { + return new ThisNode($2); }) ], // The range literal. @@ -321,13 +327,13 @@ }), o("INDENT Expression", function() { return [$2]; }), o("ArgList , Expression", function() { - return $1.push($3); + return $1.concat([$3]); }), o("ArgList TERMINATOR Expression", function() { - return $1.push($3); + return $1.concat([$3]); }), o("ArgList , TERMINATOR Expression", function() { - return $1.push($4); + return $1.concat([$4]); }), o("ArgList , INDENT Expression", function() { - return $1.push($4); + return $1.concat([$4]); }), o("ArgList OUTDENT", function() { return $1; }) @@ -351,7 +357,7 @@ }) ], // A catch clause. - Catch: [o("CATCH IDENTIFIER Block", function() { + Catch: [o("CATCH Identifier Block", function() { return [$2, $3]; }) ], @@ -383,9 +389,9 @@ }) ], // An array comprehension has variables for the current element and index. - ForVariables: [o("IDENTIFIER", function() { + ForVariables: [o("Identifier", function() { return [$1]; - }), o("IDENTIFIER , IDENTIFIER", function() { + }), o("Identifier , Identifier", function() { return [$1, $3]; }) ], diff --git a/src/lexer.coffee b/src/lexer.coffee index 1b389bda..d09512d2 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -21,7 +21,7 @@ KEYWORDS: [ "super", "extends" ] -# Token matching regexes. +# Token matching regexes. (keep the IDENTIFIER regex in sync with AssignNode.) IDENTIFIER : /^([a-zA-Z$_](\w|\$)*)/ NUMBER : /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i STRING : /^(""|''|"([\s\S]*?)([^\\]|\\\\)"|'([\s\S]*?)([^\\]|\\\\)')/ diff --git a/src/nodes.coffee b/src/nodes.coffee index 89208fba..0f12954b 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -153,8 +153,7 @@ Node::top_sensitive: -> false Expressions: exports.Expressions: inherit Node, { constructor: (nodes) -> - @expressions: flatten nodes - @children: @expressions + @children: @expressions: flatten nodes this # Tack an expression on to the end of this expression list. @@ -235,8 +234,7 @@ statement Expressions LiteralNode: exports.LiteralNode: inherit Node, { constructor: (value) -> - @value: value - @children: [value] + @children: [@value: value] this # Break and continue must be treated as statements -- they lose their meaning @@ -257,8 +255,7 @@ LiteralNode::is_statement_only: LiteralNode::is_statement ReturnNode: exports.ReturnNode: inherit Node, { constructor: (expression) -> - @expression: expression - @children: [expression] + @children: [@expression: expression] this compile_node: (o) -> @@ -275,9 +272,7 @@ ValueNode: exports.ValueNode: inherit Node, { SOAK: " == undefined ? undefined : " constructor: (base, properties) -> - @base: base - @properties: flatten(properties or []) - @children: flatten(@base, @properties) + @children: flatten(@base: base, @properties: (properties or [])) this push: (prop) -> @@ -356,9 +351,7 @@ statement CommentNode CallNode: exports.CallNode: inherit Node, { constructor: (variable, args) -> - @variable: variable - @args: args or [] - @children: flatten([@variable, @args]) + @children: flatten [@variable: variable, @args: (args or [])] @prefix: '' this @@ -412,9 +405,7 @@ CallNode: exports.CallNode: inherit Node, { ExtendsNode: exports.ExtendsNode: inherit Node, { constructor: (child, parent) -> - @child: child - @parent: parent - @children: [child, parent] + @children: [@child: child, @parent: parent] this # Hooking one constructor into another's prototype chain. @@ -437,8 +428,7 @@ statement ExtendsNode AccessorNode: exports.AccessorNode: inherit Node, { constructor: (name, tag) -> - @name: name - @children: [@name] + @children: [@name: name] @prototype: tag is 'prototype' @soak: tag is 'soak' this @@ -477,9 +467,7 @@ ThisNode: exports.ThisNode: inherit Node, { RangeNode: exports.RangeNode: inherit Node, { constructor: (from, to, exclusive) -> - @from: from - @to: to - @children: [from, to] + @children: [@from: from, @to: to] @exclusive: !!exclusive this @@ -526,6 +514,106 @@ SliceNode: exports.SliceNode: inherit Node, { } +# An object literal. +ObjectNode: exports.ObjectNode: inherit Node, { + + constructor: (props) -> + @objects: @properties: props or [] + this + + # All the mucking about with commas is to make sure that CommentNodes and + # AssignNodes get interleaved correctly, with no trailing commas or + # commas affixed to comments. TODO: Extract this and add it to ArrayNode. + compile_node: (o) -> + o.indent: @idt(1) + non_comments: prop for prop in @properties when not (prop instanceof CommentNode) + last_noncom: non_comments[non_comments.length - 1] + props: for prop, i in @properties + join: ",\n" + join: "\n" if prop is last_noncom or prop instanceof CommentNode + join: '' if i is non_comments.length - 1 + indent: if prop instanceof CommentNode then '' else @idt(1) + indent + prop.compile(o) + join + '{\n' + props.join('') + '\n' + @idt() + '}' + +} + + + + + + + +# Setting the value of a local variable, or the value of an object property. +AssignNode: exports.AssignNode: inherit Node, { + + # Keep the identifier regex in sync with the Lexer. + IDENTIFIER: /^([a-zA-Z$_](\w|\$)*)/ + PROTO_ASSIGN: /^(\S+)\.prototype/ + LEADING_DOT: /^\.(prototype\.)?/ + + constructor: (variable, value, context) -> + @children: [@variable: variable, @value: value] + @context: context + this + + top_sensitive: -> + true + + is_value: -> + @variable instanceof ValueNode + + is_statement: -> + @is_value() and (@variable.is_array() or @variable.is_object()) + + compile_node: (o) -> + top: del o, 'top' + return @compile_pattern_match(o) if @is_statement() + return @compile_splice(o) if @is_value() and @variable.is_splice() + stmt: del o, 'as_statement' + name: @variable.compile(o) + last: if @is_value() then @variable.last.replace(@LEADING_DOT, '') else name + match: name.match(@PROTO_ASSIGN) + proto: match and match[1] + if @value instanceof CodeNode + @value.name: last if last.match(@IDENTIFIER) + @value.proto: proto if proto + return name + ': ' + @value.compile(o) if @context is 'object' + o.scope.find(name) unless @is_value() and @variable.has_properties() + val: name + ' = ' + @value.compile(o) + return @idt() + val + ';' if stmt + val: '(' + val + ')' if not top or o.returns + val: @idt() + 'return ' + val if o.returns + val + + # Implementation of recursive pattern matching, when assigning array or + # object literals to a value. Peeks at their properties to assign inner names. + # See: http://wiki.ecmascript.org/doku.php?id=harmony:destructuring + compile_pattern_match: (o) -> + val_var: o.scope.free_variable() + assigns: [@idt() + val_var + ' = ' + @value.compile(o) + ';'] + o.top: true + o.as_statement: true + for obj, i in @variable.base.objects + [obj, i]: [obj.value, obj.variable.base] if @variable.is_object() + access_class: if @variable.is_array() then IndexNode else AccessorNode + if obj instanceof SplatNode + val: new LiteralNode(obj.compile_value(o, val_var, @variable.base.objects.indexOf(obj))) + else + val: new ValueNode(val_var, [new access_class(new LiteralNode(i))]) + assigns.push(new AssignNode(obj, val).compile(o)) + assigns.join("\n") + + compile_splice: (o) -> + name: @variable.compile(merge(o, {only_first: true})) + range: @variable.properties.last.range + plus: if range.exclusive then '' else ' + 1' + from: range.from.compile(o) + to: range.to.compile(o) + ' - ' + from + plus + name + '.splice.apply(' + name + ', [' + from + ', ' + to + '].concat(' + @value.compile(o) + '))' + +} + diff --git a/src/parser.coffee b/src/parser.coffee index 2b44d209..90a2bb12 100644 --- a/src/parser.coffee +++ b/src/parser.coffee @@ -86,10 +86,18 @@ grammar: { o "INDENT OUTDENT", -> new Expressions() ] - # All hard-coded values. These can be printed straight to JavaScript. - Literal: [ + Identifier: [ + o "IDENTIFIER", -> new LiteralNode(yytext) + ] + + AlphaNumeric: [ o "NUMBER", -> new LiteralNode(yytext) o "STRING", -> new LiteralNode(yytext) + ] + + # All hard-coded values. These can be printed straight to JavaScript. + Literal: [ + o "AlphaNumeric", -> $1 o "JS", -> new LiteralNode(yytext) o "REGEX", -> new LiteralNode(yytext) o "BREAK", -> new LiteralNode(yytext) @@ -110,9 +118,8 @@ grammar: { # Assignment within an object literal (can be quoted). AssignObj: [ - o "IDENTIFIER ASSIGN Expression", -> new AssignNode(new ValueNode(new LiteralNode(yytext)), $3, 'object') - o "STRING ASSIGN Expression", -> new AssignNode(new ValueNode(new LiteralNode(yytext)), $3, 'object') - o "NUMBER ASSIGN Expression", -> new AssignNode(new ValueNode(new LiteralNode(yytext)), $3, 'object') + o "Identifier ASSIGN Expression", -> new AssignNode(new ValueNode($1), $3, 'object') + o "AlphaNumeric ASSIGN Expression", -> new AssignNode(new ValueNode($1), $3, 'object') o "Comment" ] @@ -224,7 +231,7 @@ grammar: { # Expressions that can be treated as values. Value: [ - o "IDENTIFIER", -> new ValueNode(new LiteralNode(yytext)) + o "Identifier", -> new ValueNode($1) o "Literal", -> new ValueNode($1) o "Array", -> new ValueNode($1) o "Object", -> new ValueNode($1) @@ -237,9 +244,9 @@ grammar: { # Accessing into an object or array, through dot or index notation. Accessor: [ - o "PROPERTY_ACCESS IDENTIFIER", -> new AccessorNode(new LiteralNode(yytext)) - o "PROTOTYPE_ACCESS IDENTIFIER", -> new AccessorNode(new LiteralNode(yytext), 'prototype') - o "SOAK_ACCESS IDENTIFIER", -> new AccessorNode(new LiteralNode(yytext), 'soak') + o "PROPERTY_ACCESS Identifier", -> new AccessorNode($2) + o "PROTOTYPE_ACCESS Identifier", -> new AccessorNode($2, 'prototype') + o "SOAK_ACCESS Identifier", -> new AccessorNode($2, 'soak') o "Index" o "Slice", -> new SliceNode($1) ] @@ -258,9 +265,9 @@ grammar: { AssignList: [ o "", -> [] o "AssignObj", -> [$1] - o "AssignList , AssignObj", -> $1.push $3 - o "AssignList TERMINATOR AssignObj", -> $1.push $3 - o "AssignList , TERMINATOR AssignObj", -> $1.push $4 + o "AssignList , AssignObj", -> $1.concat [$3] + o "AssignList TERMINATOR AssignObj", -> $1.concat [$3] + o "AssignList , TERMINATOR AssignObj", -> $1.concat [$4] o "INDENT AssignList OUTDENT", -> $2 ] @@ -295,7 +302,7 @@ grammar: { # This references, either naked or to a property. This: [ o "@", -> new ThisNode() - o "@ IDENTIFIER", -> new ThisNode(yytext) + o "@ Identifier", -> new ThisNode($2) ] # The range literal. @@ -320,10 +327,10 @@ grammar: { o "", -> [] o "Expression", -> [$1] o "INDENT Expression", -> [$2] - o "ArgList , Expression", -> $1.push $3 - o "ArgList TERMINATOR Expression", -> $1.push $3 - o "ArgList , TERMINATOR Expression", -> $1.push $4 - o "ArgList , INDENT Expression", -> $1.push $4 + o "ArgList , Expression", -> $1.concat [$3] + o "ArgList TERMINATOR Expression", -> $1.concat [$3] + o "ArgList , TERMINATOR Expression", -> $1.concat [$4] + o "ArgList , INDENT Expression", -> $1.concat [$4] o "ArgList OUTDENT", -> $1 ] @@ -343,7 +350,7 @@ grammar: { # A catch clause. Catch: [ - o "CATCH IDENTIFIER Block", -> [$2, $3] + o "CATCH Identifier Block", -> [$2, $3] ] # Throw an exception. @@ -372,8 +379,8 @@ grammar: { # An array comprehension has variables for the current element and index. ForVariables: [ - o "IDENTIFIER", -> [$1] - o "IDENTIFIER , IDENTIFIER", -> [$1, $3] + o "Identifier", -> [$1] + o "Identifier , Identifier", -> [$1, $3] ] # The source of the array comprehension can optionally be filtered.