diff --git a/lib/coffee_script/nodes.js b/lib/coffee_script/nodes.js index bb9b3d41..43228f6b 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, 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 AccessorNode, ArrayNode, AssignNode, CallNode, ClosureNode, CodeNode, CommentNode, ExistenceNode, Expressions, ExtendsNode, ForNode, IDENTIFIER, IfNode, 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. @@ -1290,4 +1290,113 @@ } })); statement(ForNode); + // If/else statements. Switch/whens get compiled into these. Acts as an + // expression by pushing down requested returns to the expression bodies. + // Single-expression IfNodes are compiled into ternary operators if possible, + // because ternaries are first-class returnable assignable expressions. + IfNode = (exports.IfNode = inherit(Node, { + constructor: function constructor(condition, body, else_body, tags) { + this.condition = condition; + this.body = body && body.unwrap(); + this.else_body = else_body && else_body.unwrap(); + this.children = [this.condition, this.body, this.else_body]; + this.tags = tags || { + }; + if (this.condition instanceof Array) { + this.multiple = true; + } + if (this.tags.invert) { + this.condition = new OpNode('!', new ParentheticalNode(this.condition)); + } + return this; + }, + push: function push(else_body) { + var eb; + eb = else_body.unwrap(); + this.else_body ? this.else_body.push(eb) : (this.else_body = eb); + return this; + }, + force_statement: function force_statement() { + this.tags.statement = true; + return this; + }, + // Rewrite a chain of IfNodes with their switch condition for equality. + rewrite_condition: function rewrite_condition(expression) { + var __a, __b, __c, cond; + this.condition = (function() { + if (this.multiple) { + __a = []; __b = this.condition; + for (__c = 0; __c < __b.length; __c++) { + cond = __b[__c]; + __a.push(new OpNode('is', expression, cond)); + } + return __a; + } else { + return new OpNode('is', expression, this.condition); + } + }).call(this); + if (this.is_chain()) { + this.else_body.rewrite_condition(expression); + } + return this; + }, + // Rewrite a chain of IfNodes to add a default case as the final else. + add_else: function add_else(exprs) { + this.is_chain() ? this.else_body.add_else(exprs) : (this.else_body = exprs && exprs.unwrap()); + return this; + }, + // If the else_body is an IfNode itself, then we've got an if-else chain. + is_chain: function is_chain() { + return this.chain = this.chain || this.else_body && this.else_body instanceof IfNode; + }, + // The IfNode only compiles into a statement if either of the bodies needs + // to be a statement. + is_statement: function is_statement() { + return this.statement = this.statement || !!(this.comment || this.tags.statement || this.body.is_statement() || (this.else_body && this.else_body.is_statement())); + }, + compile_condition: function compile_condition(o) { + var __a, __b, __c, cond; + return ((function() { + __a = []; __b = flatten(this.condition); + for (__c = 0; __c < __b.length; __c++) { + cond = __b[__c]; + __a.push(cond.compile(o)); + } + return __a; + }).call(this)).join(' || '); + }, + compile_node: function compile_node(o) { + return this.is_statement() ? this.compile_statement(o) : this.compile_ternary(o); + }, + // Compile the IfNode as a regular if-else statement. Flattened chains + // force sub-else bodies into statement form. + compile_statement: function compile_statement(o) { + var body, child, com_dent, cond_o, else_part, if_dent, if_part, prefix; + child = del(o, 'chain_child'); + cond_o = dup(o); + del(cond_o, 'returns'); + o.indent = this.idt(1); + o.top = true; + if_dent = child ? '' : this.idt(); + com_dent = child ? this.idt() : ''; + prefix = this.comment ? this.comment.compile(cond_o) + '\n' + com_dent : ''; + body = Expressions.wrap([body]).compile(o); + if_part = prefix + if_dent + 'if (' + compile_condition(cond_o) + ') {\n' + body + '\n' + this.idt() + '}'; + if (!(this.else_body)) { + return if_part; + } + else_part = this.is_chain() ? ' else ' + this.else_body.compile(merge(o, { + indent: this.idt(), + chain_child: true + })) : ' else {\n' + Expressions.wrap(this.else_body).compile(o) + '\n' + this.idt() + '}'; + return if_part + else_part; + }, + // Compile the IfNode into a ternary operator. + compile_ternary: function compile_ternary(o) { + var else_part, if_part; + if_part = this.condition.compile(o) + ' ? ' + this.body.compile(o); + else_part = this.else_body ? this.else_body.compile(o) : 'null'; + return if_part + ' : ' + else_part; + } + })); })(); \ No newline at end of file diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 35984b51..b6906ec7 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -1029,7 +1029,7 @@ module CoffeeScript # Compile the IfNode into a ternary operator. def compile_ternary(o) if_part = "#{@condition.compile(o)} ? #{@body.compile(o)}" - else_part = @else_body ? "#{@else_body.compile(o)}" : 'null' + else_part = @else_body ? @else_body.compile(o) : 'null' "#{if_part} : #{else_part}" end end diff --git a/lib/coffee_script/parser.js b/lib/coffee_script/parser.js index 7ff8c3df..4d5732f9 100644 --- a/lib/coffee_script/parser.js +++ b/lib/coffee_script/parser.js @@ -462,7 +462,7 @@ statement: true }); }), o("Comment TERMINATOR When", function() { - return $3.add_comment($1); + return $3.comment = $1; }) ], // The most basic form of "if". diff --git a/src/nodes.coffee b/src/nodes.coffee index d6af3294..f5eee957 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -945,7 +945,87 @@ ForNode: exports.ForNode: inherit Node, { statement ForNode - +# If/else statements. Switch/whens get compiled into these. Acts as an +# expression by pushing down requested returns to the expression bodies. +# Single-expression IfNodes are compiled into ternary operators if possible, +# because ternaries are first-class returnable assignable expressions. +IfNode: exports.IfNode: inherit Node, { + + constructor: (condition, body, else_body, tags) -> + @condition: condition + @body: body and body.unwrap() + @else_body: else_body and else_body.unwrap() + @children: [@condition, @body, @else_body] + @tags: tags or {} + @multiple: true if @condition instanceof Array + @condition: new OpNode('!', new ParentheticalNode(@condition)) if @tags.invert + this + + push: (else_body) -> + eb: else_body.unwrap() + if @else_body then @else_body.push(eb) else @else_body: eb + this + + force_statement: -> + @tags.statement: true + this + + # Rewrite a chain of IfNodes with their switch condition for equality. + rewrite_condition: (expression) -> + @condition: if @multiple + new OpNode('is', expression, cond) for cond in @condition + else + new OpNode('is', expression, @condition) + @else_body.rewrite_condition(expression) if @is_chain() + this + + # Rewrite a chain of IfNodes to add a default case as the final else. + add_else: (exprs) -> + if @is_chain() then @else_body.add_else(exprs) else @else_body: exprs and exprs.unwrap() + this + + # If the else_body is an IfNode itself, then we've got an if-else chain. + is_chain: -> + @chain ||= @else_body and @else_body instanceof IfNode + + # The IfNode only compiles into a statement if either of the bodies needs + # to be a statement. + is_statement: -> + @statement ||= !!(@comment or @tags.statement or @body.is_statement() or (@else_body and @else_body.is_statement())) + + compile_condition: (o) -> + (cond.compile(o) for cond in flatten(@condition)).join(' || ') + + compile_node: (o) -> + if @is_statement() then @compile_statement(o) else @compile_ternary(o) + + # Compile the IfNode as a regular if-else statement. Flattened chains + # force sub-else bodies into statement form. + compile_statement: (o) -> + child: del o, 'chain_child' + cond_o: dup o + del cond_o, 'returns' + o.indent: @idt(1) + o.top: true + if_dent: if child then '' else @idt() + com_dent: if child then @idt() else '' + prefix: if @comment then @comment.compile(cond_o) + '\n' + com_dent else '' + body: Expressions.wrap([body]).compile(o) + if_part: prefix + if_dent + 'if (' + compile_condition(cond_o) + ') {\n' + body + '\n' + @idt() + '}' + return if_part unless @else_body + else_part: if @is_chain() + ' else ' + @else_body.compile(merge(o, {indent: @idt(), chain_child: true})) + else + ' else {\n' + Expressions.wrap(@else_body).compile(o) + '\n' + @idt() + '}' + if_part + else_part + + # Compile the IfNode into a ternary operator. + compile_ternary: (o) -> + if_part: @condition.compile(o) + ' ? ' + @body.compile(o) + else_part: if @else_body then @else_body.compile(o) else 'null' + if_part + ' : ' + else_part + +} diff --git a/src/parser.coffee b/src/parser.coffee index 072b85fa..1ddb10ea 100644 --- a/src/parser.coffee +++ b/src/parser.coffee @@ -405,7 +405,7 @@ grammar: { When: [ o "LEADING_WHEN SimpleArgs Block", -> new IfNode($2, $3, null, {statement: true}) o "LEADING_WHEN SimpleArgs Block TERMINATOR", -> new IfNode($2, $3, null, {statement: true}) - o "Comment TERMINATOR When", -> $3.add_comment($1) + o "Comment TERMINATOR When", -> $3.comment: $1 ] # The most basic form of "if".