diff --git a/TODO b/TODO index 7fca8cc4..7761c3a3 100644 --- a/TODO +++ b/TODO @@ -2,6 +2,13 @@ TODO: * Finish the examples. +* Write a test suite that checks the JS evaluation. + +* Figure out a generic way to transform statements into expressions, and + use it recursively for returns and assigns on whiles, fors, ifs, etc. + +* If we manage to get array comprehensions working ... object comprehensions? + * Create the documentation page. (amy, idle) uv -c . -s coffeescript -t amy --no-lines examples/code.cs > code.html diff --git a/examples/code.cs b/examples/code.cs index a06dbb7e..4504aba9 100644 --- a/examples/code.cs +++ b/examples/code.cs @@ -109,9 +109,9 @@ change_a_and_set_b: => b: 20 # Array comprehensions. -supper: food.capitalize() for food in ['toast', 'cheese', 'wine']. +supper: [food.capitalize() for food in ['toast', 'cheese', 'wine']] -drink(bottle) for bottle, i in ['soda', 'wine', 'lemonade'] if even(i). +[drink(bottle) for bottle, i in ['soda', 'wine', 'lemonade'] if even(i)] # Switch statements ("else" serves as a default). switch day diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index d01c38d4..d2999541 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -26,14 +26,14 @@ prechigh left ':' right '-=' '+=' '/=' '*=' '||=' '&&=' right DELETE - right RETURN THROW FOR WHILE + right RETURN THROW FOR IN WHILE left UNLESS IF ELSE nonassoc "." preclow -# We expect 2 shift/reduce errors for optional syntax. +# We expect 4 shift/reduce errors for optional syntax. # There used to be 252 -- greatly improved. -expect 2 +expect 4 rule @@ -54,12 +54,22 @@ rule # All types of expressions in our language. Expression: + PureExpression + | Statement + ; + + # The parts that are natural JavaScript expressions. + PureExpression: Literal | Value | Call - | Assign | Code | Operation + ; + + # We have to take extra care to convert these statements into expressions. + Statement: + Assign | If | Try | Throw @@ -267,17 +277,17 @@ rule # Array comprehensions, including guard and current index. For: Expression FOR IDENTIFIER - IN Expression "." { result = ForNode.new(val[0], val[4], val[2]) } + IN PureExpression "." { result = ForNode.new(val[0], val[4], val[2], nil) } | Expression FOR IDENTIFIER "," IDENTIFIER - IN Expression "." { result = ForNode.new(val[0], val[6], val[2], val[4]) } + IN PureExpression "." { result = ForNode.new(val[0], val[6], val[2], nil, val[4]) } | Expression FOR IDENTIFIER - IN Expression - IF Expression "." { result = ForNode.new(IfNode.new(val[6], Expressions.new([val[0]])), val[4], val[2]) } + IN PureExpression + IF Expression "." { result = ForNode.new(val[0], val[4], val[2], val[6]) } | Expression FOR IDENTIFIER "," IDENTIFIER - IN Expression - IF Expression "." { result = ForNode.new(IfNode.new(val[8], Expressions.new([val[0]])), val[6], val[2], val[4]) } + IN PureExpression + IF Expression "." { result = ForNode.new(val[0], val[6], val[2], val[8], val[4]) } ; # Switch/Case blocks. diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 071324d8..66c8f1d3 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -169,9 +169,21 @@ module CoffeeScript return !@properties.empty? end + def statement? + @literal.is_a?(Node) && @literal.statement? && !properties? + end + + def custom_assign? + @literal.is_a?(Node) && @literal.custom_assign? && !properties? + end + + def custom_return? + @literal.is_a?(Node) && @literal.custom_return? && !properties? + end + def compile(indent, scope, opts={}) parts = [@literal, @properties].flatten.map do |v| - v.respond_to?(:compile) ? v.compile(indent, scope) : v.to_s + v.respond_to?(:compile) ? v.compile(indent, scope, opts) : v.to_s end @last = parts.last parts.join('') @@ -364,10 +376,10 @@ module CoffeeScript custom_return custom_assign - attr_reader :body, :source, :name, :index + attr_reader :body, :source, :name, :filter, :index - def initialize(body, source, name, index=nil) - @body, @source, @name, @index = body, source, name, index + def initialize(body, source, name, filter, index=nil) + @body, @source, @name, @filter, @index = body, source, name, filter, index end def line_ending @@ -388,18 +400,28 @@ module CoffeeScript set_result = '' save_result = '' return_result = '' + body = @body + suffix = ';' if opts[:return] || opts[:assign] rvar = scope.free_variable set_result = "var #{rvar} = [];\n#{indent}" - save_result = "#{rvar}[#{ivar}] = " + save_result += "#{rvar}[#{ivar}] = " return_result = rvar return_result = "#{opts[:assign]} = #{return_result}" if opts[:assign] return_result = "return #{return_result}" if opts[:return] return_result = "\n#{indent}#{return_result}" + if @filter + body = CallNode.new(ValueNode.new(LiteralNode.new(rvar), [AccessorNode.new('push')]), [@body]) + body = @filter ? IfNode.new(@filter, body, nil, :statement) : body + save_result = '' + suffix = '' + end + elsif @filter + body = IfNode.new(@filter, @body) end - body = @body.compile(indent + TAB, scope) - "#{source_part}\n#{indent}#{set_result}for (#{for_part}) {#{var_part}#{index_part}#{indent + TAB}#{save_result}#{body};\n#{indent}}#{return_result}" + body = body.compile(indent + TAB, scope) + "#{source_part}\n#{indent}#{set_result}for (#{for_part}) {#{var_part}#{index_part}#{indent + TAB}#{save_result}#{body}#{suffix}\n#{indent}}#{return_result}" end end @@ -444,13 +466,25 @@ module CoffeeScript attr_reader :expressions def initialize(expressions) - @expressions = expressions + @expressions = expressions.unwrap + end + + def statement? + @expressions.statement? + end + + def custom_assign? + @expressions.custom_assign? + end + + def custom_return? + @expressions.custom_return? end def compile(indent, scope, opts={}) - compiled = @expressions.unwrap.compile(indent, scope) + compiled = @expressions.compile(indent, scope, opts) compiled = compiled[0...-1] if compiled[-1..-1] == ';' - opts[:no_paren] ? compiled : "(#{compiled})" + opts[:no_paren] || statement? ? compiled : "(#{compiled})" end end @@ -465,7 +499,8 @@ module CoffeeScript @condition = condition @body = body && body.unwrap @else_body = else_body && else_body.unwrap - @condition = OpNode.new("!", @condition) if tag == :invert + @tag = tag + @condition = OpNode.new("!", @condition) if @tag == :invert end def <<(else_body) @@ -495,7 +530,7 @@ module CoffeeScript # The IfNode only compiles into a statement if either of the bodies needs # to be a statement. def statement? - @is_statement ||= (@body.statement? || (@else_body && @else_body.statement?)) + @is_statement ||= ((@tag == :statement) || @body.statement? || (@else_body && @else_body.statement?)) end def custom_return? diff --git a/test/unit/test_parser.rb b/test/unit/test_parser.rb index 64825506..d7c1f457 100644 --- a/test/unit/test_parser.rb +++ b/test/unit/test_parser.rb @@ -36,7 +36,7 @@ class ParserTest < Test::Unit::TestCase assert body.operator == '*' end - def test_lexing_if_statement + def test_parsing_if_statement the_if = @par.parse("clap_your_hands() if happy").expressions.first assert the_if.is_a? IfNode assert the_if.condition.literal == 'happy' @@ -44,6 +44,14 @@ class ParserTest < Test::Unit::TestCase assert the_if.body.variable.literal == 'clap_your_hands' end + def test_parsing_array_comprehension + nodes = @par.parse("i for x, i in [10, 9, 8, 7, 6, 5] if i % 2 is 0.").expressions + assert nodes.first.is_a? ForNode + assert nodes.first.body.literal == 'i' + assert nodes.first.filter.operator == '===' + assert nodes.first.source.literal.objects.last.value == "5" + end + def test_parsing nodes = @par.parse(File.read('test/fixtures/each.cs')) assign = nodes.expressions.first