From 13d3b3a3ce513cd0fa9a9dc31071fcea4ef4cfb8 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 10 Feb 2010 20:19:59 -0500 Subject: [PATCH] now self-compiling array/object/range comprehensions. --- lib/coffee_script/nodes.js | 135 +++++++++++++++++++++++++++++++++---- lib/coffee_script/nodes.rb | 3 +- src/nodes.coffee | 97 +++++++++++++++++++++++--- 3 files changed, 209 insertions(+), 26 deletions(-) diff --git a/lib/coffee_script/nodes.js b/lib/coffee_script/nodes.js index d7989820..bb9b3d41 100644 --- a/lib/coffee_script/nodes.js +++ b/lib/coffee_script/nodes.js @@ -1,5 +1,5 @@ (function(){ - var AccessorNode, ArrayNode, AssignNode, CallNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, IDENTIFIER, IndexNode, LiteralNode, Node, ObjectNode, OpNode, ParentheticalNode, PushNode, RangeNode, ReturnNode, SliceNode, SplatNode, TAB, TRAILING_WHITESPACE, ThisNode, ThrowNode, TryNode, ValueNode, WhileNode, any, compact, del, dup, flatten, inherit, merge, statement; + var AccessorNode, ArrayNode, AssignNode, CallNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, ForNode, IDENTIFIER, IndexNode, LiteralNode, Node, ObjectNode, OpNode, ParentheticalNode, PushNode, RangeNode, ReturnNode, SliceNode, SplatNode, TAB, TRAILING_WHITESPACE, ThisNode, ThrowNode, TryNode, ValueNode, WhileNode, 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. @@ -144,9 +144,6 @@ this.name = this.constructor.name; return this.values = arguments; }; - exports.Expressions.wrap = function wrap(values) { - return this.values = values; - }; // Some helper functions // Tabs are two spaces for pretty printing. TAB = ' '; @@ -205,15 +202,24 @@ }; // Merge objects. merge = function merge(src, dest) { - var __a, key, val; + var __a, __b, fresh, key, val; + fresh = { + }; __a = src; for (key in __a) { val = __a[key]; if (__hasProp.call(__a, key)) { - ((dest[key] = val)); + ((fresh[key] = val)); } } - return dest; + __b = dest; + for (key in __b) { + val = __b[key]; + if (__hasProp.call(__b, key)) { + ((fresh[key] = val)); + } + } + return fresh; }; // Do any of the elements in the list pass a truth test? any = function any(list, test) { @@ -721,10 +727,11 @@ // TODO: This generates pretty ugly code ... shrink it. compile_array: function compile_array(o) { var arr, body; - body = Expressions.wrap(new LiteralNode('i')); - arr = Expressions.wrap(new ForNode(body, { - source: (new ValueNode(this)) - }, 'i')); + body = Expressions.wrap([new LiteralNode('i')]); + arr = Expressions.wrap([new ForNode(body, { + source: (new ValueNode(this)) + }, 'i') + ]); return (new ParentheticalNode(new CallNode(new CodeNode([], arr)))).compile(o); } })); @@ -830,16 +837,16 @@ })) { return expressions; } - return Expressions.wrap(new CallNode(new ValueNode(new LiteralNode(array), [new AccessorNode(new LiteralNode('push'))]), [expr])); + return Expressions.wrap([new CallNode(new ValueNode(new LiteralNode(array), [new AccessorNode(new LiteralNode('push'))]), [expr])]); } }); // A faux-node used to wrap an expressions body in a closure. ClosureNode = (exports.ClosureNode = { wrap: function wrap(expressions, statement) { var call, func; - func = new ParentheticalNode(new CodeNode([], Expressions.wrap(expressions))); + func = new ParentheticalNode(new CodeNode([], Expressions.wrap([expressions]))); call = new CallNode(new ValueNode(func, new AccessorNode(new LiteralNode('call'))), [new LiteralNode('this')]); - return statement ? Expressions.wrap(call) : call; + return statement ? Expressions.wrap([call]) : call; } }); // Setting the value of a local variable, or the value of an object property. @@ -1183,4 +1190,104 @@ return '(' + code + ')'; } })); + // The replacement for the for loop is an array comprehension (that compiles) + // into a for loop. Also acts as an expression, able to return the result + // of the comprehenion. Unlike Python array comprehensions, it's able to pass + // the current index of the loop as a second parameter. + ForNode = (exports.ForNode = inherit(Node, { + constructor: function constructor(body, source, name, index) { + var __a; + this.body = body; + this.name = name; + this.index = index || null; + this.source = source.source; + this.filter = source.filter; + this.step = source.step; + this.object = !!source.object; + if (this.object) { + __a = [this.index, this.name]; + this.name = __a[0]; + this.index = __a[1]; + } + this.children = [this.body, this.source, this.filter]; + return this; + }, + top_sensitive: function top_sensitive() { + return true; + }, + compile_node: function compile_node(o) { + var body, body_dent, call, for_part, index, index_found, index_var, ivar, name, name_found, range, return_result, rvar, scope, set_result, source, source_part, step_part, svar, top_level, var_part, vars; + top_level = del(o, 'top') && !o.returns; + range = this.source instanceof ValueNode && this.source.base instanceof RangeNode && !this.source.properties.length; + source = range ? this.source.base : this.source; + scope = o.scope; + name = this.name && this.name.compile(o); + index = this.index && this.index.compile(o); + name_found = name && scope.find(name); + index_found = index && scope.find(index); + body_dent = this.idt(1); + if (!(top_level)) { + rvar = scope.free_variable(); + } + svar = scope.free_variable(); + ivar = range ? name : index || scope.free_variable(); + var_part = ''; + body = Expressions.wrap([this.body]); + if (range) { + index_var = scope.free_variable(); + source_part = source.compile_variables(o); + for_part = index_var + '=0, ' + source.compile(merge(o, { + index: ivar, + step: this.step + })) + ', ' + index_var + '++'; + } else { + index_var = null; + source_part = svar + ' = ' + this.source.compile(o) + ';\n' + this.idt(); + step_part = this.step ? ivar + ' += ' + this.step.compile(o) : ivar + '++'; + for_part = this.object ? ivar + ' in ' + svar : ivar + ' = 0; ' + ivar + ' < ' + svar + '.length; ' + step_part; + if (name) { + var_part = body_dent + name + ' = ' + svar + '[' + ivar + '];\n'; + } + } + set_result = rvar ? this.idt() + rvar + ' = []; ' : this.idt(); + return_result = rvar || ''; + if (top_level && this.contains(function(n) { + return n instanceof CodeNode; + })) { + body = ClosureNode.wrap(body, true); + } + if (!(top_level)) { + body = PushNode.wrap(rvar, body); + } + if (o.returns) { + return_result = 'return ' + return_result; + del(o, 'returns'); + if (this.filter) { + body = new IfNode(this.filter, body, null, { + statement: true + }); + } + } else if (this.filter) { + body = Expressions.wrap([new IfNode(this.filter, body)]); + } + if (this.object) { + o.scope.assign('__hasProp', 'Object.prototype.hasOwnProperty', true); + call = new CallNode(new ValueNode(new LiteralNode('__hasProp'), [new AccessorNode(new LiteralNode('call'))]), [new LiteralNode(svar), new LiteralNode(ivar)]); + body = Expressions.wrap([new IfNode(call, Expressions.wrap([body]), null, { + statement: true + }) + ]); + } + if (!(top_level)) { + return_result = '\n' + this.idt() + return_result + ';'; + } + body = body.compile(merge(o, { + indent: body_dent, + top: true + })); + vars = range ? name : name + ', ' + ivar; + return set_result + source_part + 'for (' + for_part + ') {\n' + var_part + body + '\n' + this.idt() + '}\n' + this.idt() + return_result; + } + })); + statement(ForNode); })(); \ No newline at end of file diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 99977810..35984b51 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -911,14 +911,13 @@ module CoffeeScript step_part = @step ? "#{ivar} += #{@step.compile(o)}" : "#{ivar}++" for_part = @object ? "#{ivar} in #{svar}" : "#{ivar} = 0; #{ivar} < #{svar}.length; #{step_part}" var_part = "#{body_dent}#{@name} = #{svar}[#{ivar}];\n" if @name - # body.unshift(AssignNode.new(@name, ValueNode.new(svar, [IndexNode.new(ivar)]))) if @name end set_result = rvar ? "#{idt}#{rvar} = []; " : idt return_result = rvar || '' body = ClosureNode.wrap(body, true) if top_level && contains? {|n| n.is_a? CodeNode } body = PushNode.wrap(rvar, body) unless top_level if o[:return] - return_result = "return #{return_result}" if o[:return] + return_result = "return #{return_result}" o.delete(:return) body = IfNode.new(@filter, body, nil, :statement => true) if @filter elsif @filter diff --git a/src/nodes.coffee b/src/nodes.coffee index 25e065f6..d6af3294 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -36,8 +36,6 @@ exports.ExistenceNode : -> @name: this.constructor.name; @values: arguments 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 # Tabs are two spaces for pretty printing. @@ -71,8 +69,10 @@ dup: (input) -> # Merge objects. merge: (src, dest) -> - (dest[key]: val) for key, val of src - dest + fresh: {} + (fresh[key]: val) for key, val of src + (fresh[key]: val) for key, val of dest + fresh # Do any of the elements in the list pass a truth test? any: (list, test) -> @@ -494,8 +494,8 @@ RangeNode: exports.RangeNode: inherit Node, { # part of a comprehension, slice, or splice. # TODO: This generates pretty ugly code ... shrink it. compile_array: (o) -> - body: Expressions.wrap(new LiteralNode 'i') - arr: Expressions.wrap(new ForNode(body, {source: (new ValueNode(this))}, 'i')) + body: Expressions.wrap([new LiteralNode('i')]) + arr: Expressions.wrap([new ForNode(body, {source: (new ValueNode(this))}, 'i')]) (new ParentheticalNode(new CallNode(new CodeNode([], arr)))).compile(o) } @@ -571,9 +571,9 @@ PushNode: exports.PushNode: { wrap: (array, expressions) -> expr: expressions.unwrap() return expressions if expr.is_statement_only() or expr.contains (n) -> n.is_statement_only() - Expressions.wrap(new CallNode( + Expressions.wrap([new CallNode( new ValueNode(new LiteralNode(array), [new AccessorNode(new LiteralNode('push'))]), [expr] - )) + )]) } @@ -581,9 +581,9 @@ PushNode: exports.PushNode: { ClosureNode: exports.ClosureNode: { wrap: (expressions, statement) -> - func: new ParentheticalNode(new CodeNode([], Expressions.wrap(expressions))) + func: new ParentheticalNode(new CodeNode([], Expressions.wrap([expressions]))) call: new CallNode(new ValueNode(func, new AccessorNode(new LiteralNode('call'))), [new LiteralNode('this')]) - if statement then Expressions.wrap(call) else call + if statement then Expressions.wrap([call]) else call } @@ -871,6 +871,83 @@ ParentheticalNode: exports.ParentheticalNode: inherit Node, { } +# The replacement for the for loop is an array comprehension (that compiles) +# into a for loop. Also acts as an expression, able to return the result +# of the comprehenion. Unlike Python array comprehensions, it's able to pass +# the current index of the loop as a second parameter. +ForNode: exports.ForNode: inherit Node, { + + constructor: (body, source, name, index) -> + @body: body + @name: name + @index: index or null + @source: source.source + @filter: source.filter + @step: source.step + @object: !!source.object + [@name, @index]: [@index, @name] if @object + @children: [@body, @source, @filter] + this + + top_sensitive: -> + true + + compile_node: (o) -> + top_level: del(o, 'top') and not o.returns + range: @source instanceof ValueNode and @source.base instanceof RangeNode and not @source.properties.length + source: if range then @source.base else @source + scope: o.scope + name: @name and @name.compile(o) + index: @index and @index.compile(o) + name_found: name and scope.find(name) + index_found: index and scope.find(index) + body_dent: @idt(1) + rvar: scope.free_variable() unless top_level + svar: scope.free_variable() + ivar: if range then name else index or scope.free_variable() + var_part: '' + body: Expressions.wrap([@body]) + if range + index_var: scope.free_variable() + source_part: source.compile_variables(o) + for_part: index_var + '=0, ' + source.compile(merge(o, {index: ivar, step: @step})) + ', ' + index_var + '++' + else + index_var: null + source_part: svar + ' = ' + @source.compile(o) + ';\n' + @idt() + step_part: if @step then ivar + ' += ' + @step.compile(o) else ivar + '++' + for_part: if @object then ivar + ' in ' + svar else ivar + ' = 0; ' + ivar + ' < ' + svar + '.length; ' + step_part + var_part: body_dent + name + ' = ' + svar + '[' + ivar + '];\n' if name + set_result: if rvar then @idt() + rvar + ' = []; ' else @idt() + return_result: rvar or '' + body: ClosureNode.wrap(body, true) if top_level and @contains (n) -> n instanceof CodeNode + body: PushNode.wrap(rvar, body) unless top_level + if o.returns + return_result: 'return ' + return_result + del o, 'returns' + body: new IfNode(@filter, body, null, {statement: true}) if @filter + else if @filter + body: Expressions.wrap([new IfNode(@filter, body)]) + if @object + o.scope.assign('__hasProp', 'Object.prototype.hasOwnProperty', true) + call: new CallNode( + new ValueNode(new LiteralNode('__hasProp'), [new AccessorNode(new LiteralNode('call'))]) + [new LiteralNode(svar), new LiteralNode(ivar)] + ) + body: Expressions.wrap([new IfNode( + call, Expressions.wrap([body]), null, {statement: true} + )]) + return_result: '\n' + @idt() + return_result + ';' unless top_level + body: body.compile(merge(o, {indent: body_dent, top: true})) + vars: if range then name else name + ', ' + ivar + set_result + source_part + 'for (' + for_part + ') {\n' + var_part + body + '\n' + @idt() + '}\n' + @idt() + return_result + +} + +statement ForNode + + + +