From 27f7ef09af632c249b40011011885f4e3ab2aa37 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 08:49:39 -0500 Subject: [PATCH 01/16] allow leading newlines in coffee scripts --- lib/coffee_script/rewriter.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/coffee_script/rewriter.rb b/lib/coffee_script/rewriter.rb index 0325f7404f..73d1106cfd 100644 --- a/lib/coffee_script/rewriter.rb +++ b/lib/coffee_script/rewriter.rb @@ -35,6 +35,7 @@ class Rewriter def rewrite(tokens) @tokens = tokens adjust_comments + remove_leading_newlines remove_mid_expression_newlines move_commas_outside_outdents add_implicit_indentation @@ -82,6 +83,12 @@ def adjust_comments end end + # Leading newlines would introduce an ambiguity in the grammar, so we + # dispatch them here. + def remove_leading_newlines + @tokens.shift if @tokens[0][0] == "\n" + end + # Some blocks occur in the middle of expressions -- when we're expecting # this, remove their trailing newlines. def remove_mid_expression_newlines From c19647ad33686e0a2b86d08af930259998e23b21 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 08:52:44 -0500 Subject: [PATCH 02/16] adding and fixing test for empty strings --- test/fixtures/execution/test_splices.coffee | 2 ++ test/unit/test_lexer.rb | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/fixtures/execution/test_splices.coffee b/test/fixtures/execution/test_splices.coffee index 0ac1135f99..7f28d7978b 100644 --- a/test/fixtures/execution/test_splices.coffee +++ b/test/fixtures/execution/test_splices.coffee @@ -1,3 +1,5 @@ + + array: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] array[5..10]: [0, 0, 0] diff --git a/test/unit/test_lexer.rb b/test/unit/test_lexer.rb index 5a811d3435..ec08df018c 100644 --- a/test/unit/test_lexer.rb +++ b/test/unit/test_lexer.rb @@ -7,14 +7,14 @@ def setup end def test_lexing_an_empty_string - assert @lex.tokenize("") == [["\n", "\n"]] + assert @lex.tokenize("") == [] end def test_lexing_basic_assignment code = "a: 'one'\nb: [1, 2]" - assert @lex.tokenize(code) == [[:IDENTIFIER, "a"], [:ASSIGN, ":"], - [:STRING, "'one'"], ["\n", "\n"], [:IDENTIFIER, "b"], [:ASSIGN, ":"], - ["[", "["], [:NUMBER, "1"], [",", ","], [:NUMBER, "2"], ["]", "]"], + assert @lex.tokenize(code) == [[:IDENTIFIER, "a"], [:ASSIGN, ":"], + [:STRING, "'one'"], ["\n", "\n"], [:IDENTIFIER, "b"], [:ASSIGN, ":"], + ["[", "["], [:NUMBER, "1"], [",", ","], [:NUMBER, "2"], ["]", "]"], ["\n", "\n"]] end @@ -27,7 +27,7 @@ def test_lexing_object_literal def test_lexing_function_definition code = "x, y => x * y" assert @lex.tokenize(code) == [[:PARAM, "x"], [",", ","], [:PARAM, "y"], - ["=>", "=>"], [:INDENT, 2], [:IDENTIFIER, "x"], ["*", "*"], + ["=>", "=>"], [:INDENT, 2], [:IDENTIFIER, "x"], ["*", "*"], [:IDENTIFIER, "y"], [:OUTDENT, 2], ["\n", "\n"]] end From 8dfbd1a2a81b6b8ec38a379b63074a13ce686631 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 17:35:37 -0500 Subject: [PATCH 03/16] using Object.prototype.hasOwnProperty.call instead of obj.hasOwnProperty, with an alias, for Rhino and java objects --- lib/coffee_script/nodes.rb | 8 +++++--- lib/coffee_script/scope.rb | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index ff208a9e1f..3bab64fa70 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -128,8 +128,9 @@ def compile_root(o={}) # at the top. def compile_with_declarations(o={}) code = compile_node(o) - return code unless o[:scope].declarations?(self) - write("#{idt}var #{o[:scope].declared_variables.join(', ')};\n#{code}") + code = "#{idt}var #{o[:scope].compiled_assignments};\n#{code}" if o[:scope].assignments?(self) + code = "#{idt}var #{o[:scope].compiled_declarations};\n#{code}" if o[:scope].declarations?(self) + write(code) end # Compiles a single expression within the expression list. @@ -724,8 +725,9 @@ def compile_node(o) body = Expressions.wrap(IfNode.new(@filter, body)) end if @object + o[:scope].top_level_assign("__hasProp", "Object.prototype.hasOwnProperty") body = Expressions.wrap(IfNode.new( - CallNode.new(ValueNode.new(LiteralNode.wrap(svar), [AccessorNode.new(Value.new('hasOwnProperty'))]), [LiteralNode.wrap(ivar)]), + CallNode.new(ValueNode.new(LiteralNode.wrap("__hasProp"), [AccessorNode.new(Value.new('call'))]), [LiteralNode.wrap(svar), LiteralNode.wrap(ivar)]), Expressions.wrap(body), nil, {:statement => true} diff --git a/lib/coffee_script/scope.rb b/lib/coffee_script/scope.rb index 4cb61b0de8..a15865e6f5 100644 --- a/lib/coffee_script/scope.rb +++ b/lib/coffee_script/scope.rb @@ -47,15 +47,40 @@ def free_variable @temp_variable.dup end + # Ensure that an assignment is made at the top-level scope. + # Takes two strings. + def top_level_assign(name, value) + return @parent.top_level_assign(name, value) if @parent + @variables[name.to_sym] = Value.new(value) + end + def declarations?(body) !declared_variables.empty? && body == @expressions end + def assignments?(body) + !assigned_variables.empty? && body == @expressions + end + # Return the list of variables first declared in current scope. def declared_variables @variables.select {|k, v| v == :var }.map {|pair| pair[0].to_s }.sort end + # Return the list of variables that are supposed to be assigned at the top + # of scope. + def assigned_variables + @variables.select {|k, v| v.is_a?(Value) }.sort_by {|pair| pair[0].to_s } + end + + def compiled_declarations + declared_variables.join(', ') + end + + def compiled_assignments + assigned_variables.map {|name, val| "#{name} = #{val}"}.join(', ') + end + def inspect "" end From 197914bcf72597ee51b1915d92538ab4bf5760fc Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 17:44:03 -0500 Subject: [PATCH 04/16] nicer syntax error messages for newlines and indentation --- lib/coffee_script/parse_error.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/coffee_script/parse_error.rb b/lib/coffee_script/parse_error.rb index 3415ed1207..6fbc86d127 100644 --- a/lib/coffee_script/parse_error.rb +++ b/lib/coffee_script/parse_error.rb @@ -5,6 +5,12 @@ module CoffeeScript # line-number aware. class ParseError < Racc::ParseError + TOKEN_MAP = { + 'INDENT' => 'indent', + 'OUTDENT' => 'outdent', + "\n" => 'newline' + } + def initialize(token_id, value, stack) @token_id, @value, @stack = token_id, value, stack end @@ -13,7 +19,7 @@ def message line = @value.respond_to?(:line) ? @value.line : "END" line_part = "line #{line}:" id_part = @token_id != @value.inspect ? ", unexpected #{@token_id.to_s.downcase}" : "" - val_part = ['INDENT', 'OUTDENT'].include?(@token_id) ? '' : " for '#{@value.to_s}'" + val_part = " for #{TOKEN_MAP[@value.to_s] || "'#{@value}'"}" "#{line_part} syntax error#{val_part}#{id_part}" end alias_method :inspect, :message From 87e04e9952d4879dda13848e1267deba0c490438 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 17:44:37 -0500 Subject: [PATCH 05/16] nicer syntax error messages for newlines and indentation --- lib/coffee_script/grammar.y | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index 457b171369..15e78b800d 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -20,6 +20,7 @@ token INDENT OUTDENT # Declare order of operations. prechigh + right NEW left '?' nonassoc UMINUS NOT '!' '!!' '~' '++' '--' left '*' '/' '%' @@ -35,7 +36,7 @@ prechigh right INDENT left OUTDENT right WHEN LEADING_WHEN IN OF BY - right THROW FOR NEW SUPER + right THROW FOR SUPER left EXTENDS left ASSIGN '||=' '&&=' right RETURN From 844ea332741d3eb6ca543ffc9bfbd1f69011ad82 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 17:45:06 -0500 Subject: [PATCH 06/16] mistaken commit --- lib/coffee_script/grammar.y | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index 15e78b800d..457b171369 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -20,7 +20,6 @@ token INDENT OUTDENT # Declare order of operations. prechigh - right NEW left '?' nonassoc UMINUS NOT '!' '!!' '~' '++' '--' left '*' '/' '%' @@ -36,7 +35,7 @@ prechigh right INDENT left OUTDENT right WHEN LEADING_WHEN IN OF BY - right THROW FOR SUPER + right THROW FOR NEW SUPER left EXTENDS left ASSIGN '||=' '&&=' right RETURN From 914ba1c2448ee736c62d09c1a675a9230591323e Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 18:01:12 -0500 Subject: [PATCH 07/16] removing commented-out bit --- lib/coffee_script/grammar.y | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index 457b171369..f95cef481f 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -276,7 +276,6 @@ rule Invocation: Value Arguments { result = CallNode.new(val[0], val[1]) } | Invocation Arguments { result = CallNode.new(val[0], val[1]) } - # | Invocation Code { result = val[0] << val[1] } ; # The list of arguments to a function invocation. From f0d5db7e66143acb0d56cf39feae1ddf07bbc38c Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 23:06:12 -0500 Subject: [PATCH 08/16] fixing heredocs to use the left-most indent as the indentation guide -- not just the first line of the heredoc --- .../Syntaxes/CoffeeScript.tmLanguage | 2 +- lib/coffee_script/lexer.rb | 22 ++++++++++--------- test/fixtures/execution/test_heredocs.coffee | 11 +++++++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage b/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage index 311872442d..8d651fe3f2 100644 --- a/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage +++ b/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage @@ -64,7 +64,7 @@ comment match stuff like: a => … match - ([a-zA-Z0-9_?., $:*]*)\s*(=>) + ([a-zA-Z0-9_?., $*]*)\s*(=>) name meta.inline.function.coffee diff --git a/lib/coffee_script/lexer.rb b/lib/coffee_script/lexer.rb index 6c41dde154..9541548f30 100644 --- a/lib/coffee_script/lexer.rb +++ b/lib/coffee_script/lexer.rb @@ -22,7 +22,7 @@ class Lexer IDENTIFIER = /\A([a-zA-Z$_](\w|\$)*)/ NUMBER = /\A(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i STRING = /\A(""|''|"(.*?)([^\\]|\\\\)"|'(.*?)([^\\]|\\\\)')/m - HEREDOC = /\A("{6}|'{6}|"{3}\n?(\s*)(.*?)\n?(\s*)"{3}|'{3}\n?(\s*)(.*?)\n?(\s*)'{3})/m + HEREDOC = /\A("{6}|'{6}|"{3}\n?(.*?)\n?(\s*)"{3}|'{3}\n?(.*?)\n?(\s*)'{3})/m JS = /\A(``|`(.*?)([^\\]|\\\\)`)/m OPERATOR = /\A([+\*&|\/\-%=<>:!]+)/ WHITESPACE = /\A([ \t]+)/ @@ -38,6 +38,7 @@ class Lexer MULTILINER = /\n/ COMMENT_CLEANER = /(^\s*#|\n\s*$)/ NO_NEWLINE = /\A([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)\Z/ + HEREDOC_INDENT = /^\s+/ # Tokens which a regular expression will never immediately follow, but which # a division operator might. @@ -50,12 +51,12 @@ class Lexer # Scan by attempting to match tokens one character at a time. Slow and steady. def tokenize(code) - @code = code.chomp # Cleanup code by remove extra line breaks - @i = 0 # Current character position we're parsing - @line = 1 # The current line. - @indent = 0 # The current indent level. - @indents = [] # The stack of all indent levels we are currently within. - @tokens = [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value] + @code = code.chomp # Cleanup code by remove extra line breaks + @i = 0 # Current character position we're parsing + @line = 1 # The current line. + @indent = 0 # The current indent level. + @indents = [] # The stack of all indent levels we are currently within. + @tokens = [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value] while @i < @code.length @chunk = @code[@i..-1] extract_next_token @@ -114,9 +115,10 @@ def string_token # Matches heredocs, adjusting indentation to the correct level. def heredoc_token return false unless match = @chunk.match(HEREDOC) - indent = match[2] || match[5] - doc = match[3] || match[6] - doc.gsub!(/\n#{indent}/, "\\n") + doc = match[2] || match[4] + indent = doc.scan(HEREDOC_INDENT).min + doc.gsub!(/^#{indent}/, "") + doc.gsub!("\n", "\\n") doc.gsub!('"', '\\"') token(:STRING, "\"#{doc}\"") @line += match[1].count("\n") diff --git a/test/fixtures/execution/test_heredocs.coffee b/test/fixtures/execution/test_heredocs.coffee index c28ddb5d7e..b5dcaabdba 100644 --- a/test/fixtures/execution/test_heredocs.coffee +++ b/test/fixtures/execution/test_heredocs.coffee @@ -25,4 +25,13 @@ a: """ here """ -print(a is "out\nhere") \ No newline at end of file +print(a is "out\nhere") + + +a: ''' + a + b + c + ''' + +print(a is " a\n b\nc") \ No newline at end of file From ea349a1a59b3155567a9f9b153f86d276c0b47a1 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 23:26:35 -0500 Subject: [PATCH 09/16] more safety type-checks in nodes.rb --- lib/coffee_script/nodes.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 3bab64fa70..f284155172 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -448,23 +448,27 @@ def initialize(variable, value, context=nil) end def compile_node(o) - return compile_pattern_match(o) if @variable.array? || @variable.object? - return compile_splice(o) if @variable.splice? + 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 = @variable.last.to_s.sub(LEADING_DOT, '') + last = value? ? @variable.last.to_s.sub(LEADING_DOT, '') : name proto = name[PROTO_ASSIGN, 1] o = o.merge(:last_assign => last, :proto_assign => proto) o[:immediate_assign] = last if @value.is_a?(CodeNode) && last.match(Lexer::IDENTIFIER) return write("#{name}: #{@value.compile(o)}") if @context == :object - o[:scope].find(name) unless @variable.properties? + o[:scope].find(name) unless value? && @variable.properties? val = "#{name} = #{@value.compile(o)}" return write("#{idt}#{val};") if stmt write(o[:return] ? "#{idt}return (#{val})" : val) end + def value? + @variable.is_a?(ValueNode) + end + def statement? - @variable.array? || @variable.object? + value? && (@variable.array? || @variable.object?) end # Implementation of recursive pattern matching, when assigning array or From abd9ab5c7182e82802046bd1249965a47a322f2e Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 12 Jan 2010 23:49:47 -0500 Subject: [PATCH 10/16] unified ParamSplatNode and ArgSplatNode into SplatNode --- lib/coffee_script/grammar.y | 4 ++-- lib/coffee_script/nodes.rb | 27 ++++++++++++--------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index f95cef481f..e922ecf08a 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -211,12 +211,12 @@ rule # A Parameter (or ParamSplat) in a function definition. Param: PARAM - | PARAM "." "." "." { result = ParamSplatNode.new(val[0]) } + | PARAM "." "." "." { result = SplatNode.new(val[0]) } ; # A regular splat. Splat: - Expression "." "." "." { result = ArgSplatNode.new(val[0])} + Expression "." "." "." { result = SplatNode.new(val[0]) } ; # Expressions that can be treated as values. diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index f284155172..2a8605cde3 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -244,7 +244,7 @@ def prefix end def splat? - @arguments.any? {|a| a.is_a?(ArgSplatNode) } + @arguments.any? {|a| a.is_a?(SplatNode) } end def <<(argument) @@ -274,7 +274,7 @@ def compile_splat(o) obj = @variable.source || 'this' args = @arguments.map do |arg| code = arg.compile(o) - code = arg.is_a?(ArgSplatNode) ? code : "[#{code}]" + code = arg.is_a?(SplatNode) ? code : "[#{code}]" arg.equal?(@arguments.first) ? code : ".concat(#{code})" end "#{prefix}#{meth}.apply(#{obj}, #{args.join('')})" @@ -562,7 +562,7 @@ def compile_node(o) o.delete(:no_wrap) o.delete(:globals) name = o.delete(:immediate_assign) - if @params.last.is_a?(ParamSplatNode) + if @params.last.is_a?(SplatNode) splat = @params.pop splat.index = @params.length @body.unshift(splat) @@ -574,8 +574,9 @@ def compile_node(o) end end - # A parameter splat in a function definition. - class ParamSplatNode < Node + # A splat, either as a parameter to a function, an argument to a call, + # or in a destructuring assignment. + class SplatNode < Node attr_accessor :index attr_reader :name @@ -584,20 +585,16 @@ def initialize(name) end def compile_node(o={}) - o[:scope].find(@name) - write("#{@name} = Array.prototype.slice.call(arguments, #{@index})") + write(@index ? compile_param(o) : compile_arg(o)) end - end - - class ArgSplatNode < Node - attr_reader :value - def initialize(value) - @value = value + def compile_param(o) + o[:scope].find(@name) + "#{@name} = Array.prototype.slice.call(arguments, #{@index})" end - def compile_node(o={}) - write(@value.compile(o)) + def compile_arg(o) + @name.compile(o) end end From 0ceca0778c45d768893fa9662ef5bcc4425d6e5b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jan 2010 19:56:35 -0500 Subject: [PATCH 11/16] adding when clauses with multiple values --- lib/coffee_script/grammar.y | 10 ++++++++-- lib/coffee_script/nodes.rb | 10 ++++++++-- test/fixtures/execution/test_switch.coffee | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index e922ecf08a..aab2339d26 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -314,6 +314,12 @@ rule | ArgList OUTDENT { result = val[0] } ; + # Just simple, comma-separated, required arguments (no fancy syntax). + SimpleArgs: + Expression { result = val[0] } + | SimpleArgs "," Expression { result = ([val[0]] << val[2]).flatten } + ; + # Try/catch/finally exception handling blocks. Try: TRY Block Catch { result = TryNode.new(val[1], val[2][0], val[2][1]) } @@ -383,8 +389,8 @@ rule # An individual when. When: - LEADING_WHEN Expression Block { result = IfNode.new(val[1], val[2], nil, {:statement => true}) } - | LEADING_WHEN Expression Block + LEADING_WHEN SimpleArgs Block { result = IfNode.new(val[1], val[2], nil, {:statement => true}) } + | LEADING_WHEN SimpleArgs Block Terminator { result = IfNode.new(val[1], val[2], nil, {:statement => true}) } | Comment Terminator When { result = val[2].add_comment(val[0]) } ; diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 2a8605cde3..64b854e330 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -821,6 +821,7 @@ def initialize(condition, body, else_body=nil, tags={}) @body = body && body.unwrap @else_body = else_body && else_body.unwrap @tags = tags + @multiple = true if @condition.is_a?(Array) @condition = OpNode.new("!", ParentheticalNode.new(@condition)) if @tags[:invert] end @@ -842,7 +843,8 @@ def force_statement # Rewrite a chain of IfNodes with their switch condition for equality. def rewrite_condition(expression) - @condition = OpNode.new("is", expression, @condition) + @condition = @multiple ? @condition.map {|c| OpNode.new("is", expression, c) } : + OpNode.new("is", expression, @condition) @else_body.rewrite_condition(expression) if chain? self end @@ -864,6 +866,10 @@ def statement? @is_statement ||= !!(@comment || @tags[:statement] || @body.statement? || (@else_body && @else_body.statement?)) end + def compile_condition(o) + [@condition].flatten.map {|c| c.compile(o) }.join(' || ') + end + def compile_node(o) write(statement? ? compile_statement(o) : compile_ternary(o)) end @@ -879,7 +885,7 @@ def compile_statement(o) if_dent = child ? '' : idt com_dent = child ? idt : '' prefix = @comment ? @comment.compile(cond_o) + "\n#{com_dent}" : '' - if_part = "#{prefix}#{if_dent}if (#{@condition.compile(cond_o)}) {\n#{Expressions.wrap(@body).compile(o)}\n#{idt}}" + if_part = "#{prefix}#{if_dent}if (#{compile_condition(cond_o)}) {\n#{Expressions.wrap(@body).compile(o)}\n#{idt}}" return if_part unless @else_body else_part = chain? ? " else #{@else_body.compile(o.merge(:indent => idt, :chain_child => true))}" : diff --git a/test/fixtures/execution/test_switch.coffee b/test/fixtures/execution/test_switch.coffee index 9bdbf0f011..35ff3c7ab8 100644 --- a/test/fixtures/execution/test_switch.coffee +++ b/test/fixtures/execution/test_switch.coffee @@ -15,3 +15,17 @@ result: switch num else false print(result) + + +func: num => + switch num + when 2, 4, 6 + true + when 1, 3, 5 + false + else false + +print(func(2)) +print(func(6)) +print(!func(3)) +print(!func(8)) From 1e7d638435d35d6479e9117c756a8b715233f84c Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jan 2010 20:59:57 -0500 Subject: [PATCH 12/16] adding bound functions, with test --- documentation/speed.html | 72 +++++++------------ .../Syntaxes/CoffeeScript.tmLanguage | 4 +- lib/coffee_script/grammar.y | 12 +++- lib/coffee_script/lexer.rb | 4 +- lib/coffee_script/nodes.rb | 23 ++++-- lib/coffee_script/rewriter.rb | 2 +- lib/coffee_script/scope.rb | 8 +-- lib/coffee_script/value.rb | 4 ++ test/fixtures/execution/test_functions.coffee | 22 ++++++ .../execution/test_named_functions.coffee | 8 --- 10 files changed, 85 insertions(+), 74 deletions(-) create mode 100644 test/fixtures/execution/test_functions.coffee delete mode 100644 test/fixtures/execution/test_named_functions.coffee diff --git a/documentation/speed.html b/documentation/speed.html index 52d99650b6..c8406ba4b7 100644 --- a/documentation/speed.html +++ b/documentation/speed.html @@ -14,63 +14,39 @@

Quickie CoffeeScript Speed Tests

var arr = []; while (num--) arr.push(num); - JSLitmus.test('current comprehensions', function() { - __a = arr; - __c = []; - for (__b in __a) { - if (__a.hasOwnProperty(__b)) { - num = __a[__b]; - __d = num; - __c.push(num); - } - } - }); + var f1 = function f1() { + return arr; + }; - JSLitmus.test('raw for loop (best we can do)', function() { - __a = arr; - __c = new Array(__a.length); - for (__b=0; __b < __a.length; __b++) { - __c[__b] = __a[__b]; - } + JSLitmus.test('regular function', function() { + f1(); }); - JSLitmus.test('current without hasOwnProperty check', function() { - __a = arr; - __c = []; - for (__b in __a) { - num = __a[__b]; - __d = num; - __c.push(num); - } - }); + var __this = this; + + var f2 = function f2() { + return (function() { + return arr; + }).apply(__this, arguments); + }; - JSLitmus.test('raw for..in loop', function() { - __a = arr; - __c = new Array(__a.length); - for (__b in __a) { - __c[__b] = __a[__b]; - } + JSLitmus.test('bound function', function() { + f2(); }); - JSLitmus.test('weepy\'s comprehensions', function() { - __c = []; __a = arr; - __d = function(num, __b) { - __c.push(num); + var f3 = (function() { + __b = function() { + return arr; }; - if (__a instanceof Array) { - for (__b=0; __b<__a.length; __b++) __d(__a[__b], __b); - } else { - for (__b in __a) { if (__a.hasOwnProperty(__b)) __d(__a[__b], __b); } - } - }); + return (function f2() { + return __b.apply(__this, arguments); + }); + })(); - JSLitmus.test('CoffeeScript 0.2.2 comprehensions', function() { - __c = []; __a = arr; - for (__b=0; __b<__a.length; __b++) { - num = __a[__b]; - __c.push(num); - } + JSLitmus.test('prebound function', function() { + f3(); }); + diff --git a/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage b/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage index 8d651fe3f2..d171bb5ba6 100644 --- a/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage +++ b/lib/coffee_script/CoffeeScript.tmbundle/Syntaxes/CoffeeScript.tmLanguage @@ -43,7 +43,7 @@ comment match stuff like: funcName: => … match - ([a-zA-Z0-9_?.$:*]*)\s*(=|:)\s*([\w,\s]*?)\s*(=>) + ([a-zA-Z0-9_?.$:*]*?)\s*(=\b|:\b)\s*([\w,\s]*?)\s*(=+>) name meta.function.coffee @@ -64,7 +64,7 @@ comment match stuff like: a => … match - ([a-zA-Z0-9_?., $*]*)\s*(=>) + ([a-zA-Z0-9_?., $*]*)\s*(=+>) name meta.inline.function.coffee diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index aab2339d26..f6bf246646 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -39,7 +39,7 @@ prechigh left EXTENDS left ASSIGN '||=' '&&=' right RETURN - right '=>' UNLESS IF ELSE WHILE + right '=>' '==>' UNLESS IF ELSE WHILE preclow rule @@ -198,8 +198,14 @@ rule # Function definition. Code: - ParamList "=>" Block { result = CodeNode.new(val[0], val[2]) } - | "=>" Block { result = CodeNode.new([], val[1]) } + ParamList FuncGlyph Block { result = CodeNode.new(val[0], val[2], val[1]) } + | FuncGlyph Block { result = CodeNode.new([], val[1], val[0]) } + ; + + # The symbols to signify functions, and bound functions. + FuncGlyph: + '=>' { result = :func } + | '==>' { result = :boundfunc } ; # The parameters to a function definition. diff --git a/lib/coffee_script/lexer.rb b/lib/coffee_script/lexer.rb index 9541548f30..1667e2991b 100644 --- a/lib/coffee_script/lexer.rb +++ b/lib/coffee_script/lexer.rb @@ -27,7 +27,7 @@ class Lexer OPERATOR = /\A([+\*&|\/\-%=<>:!]+)/ WHITESPACE = /\A([ \t]+)/ COMMENT = /\A(((\n?[ \t]*)?#.*$)+)/ - CODE = /\A(=>)/ + CODE = /\A(=?=>)/ REGEX = /\A(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/ MULTI_DENT = /\A((\n([ \t]*))+)(\.)?/ LAST_DENT = /\n([ \t]*)/ @@ -155,7 +155,7 @@ def indent_token @line += indent.scan(MULTILINER).size @i += indent.size next_character = @chunk[MULTI_DENT, 4] - no_newlines = next_character == '.' || (last_value.to_s.match(NO_NEWLINE) && last_value != "=>") + no_newlines = next_character == '.' || (last_value.to_s.match(NO_NEWLINE) && !last_value.match(CODE)) return suppress_newlines(indent) if no_newlines size = indent.scan(LAST_DENT).last.last.length return newline_token(indent) if size == @indent diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 64b854e330..0772add883 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -546,14 +546,23 @@ def compile_unary(o) # A function definition. The only node that creates a new Scope. class CodeNode < Node - attr_reader :params, :body + attr_reader :params, :body, :bound - def initialize(params, body) + def initialize(params, body, tag=nil) @params = params - @body = body + @body = body + @bound = tag == :boundfunc + end + + def statement? + @bound end def compile_node(o) + if @bound + o[:scope].assign("__this", "this") + fvar = o[:scope].free_variable + end shared_scope = o.delete(:shared_scope) o[:scope] = shared_scope || Scope.new(o[:scope], @body) o[:return] = true @@ -568,9 +577,11 @@ def compile_node(o) @body.unshift(splat) end @params.each {|id| o[:scope].parameter(id.to_s) } - code = @body.compile_with_declarations(o) + code = "\n#{@body.compile_with_declarations(o)}\n" name_part = name ? " #{name}" : '' - write("function#{name_part}(#{@params.join(', ')}) {\n#{code}\n#{idt}}") + func = "function#{@bound ? '' : name_part}(#{@params.join(', ')}) {#{code}#{idt}}" + return write(func) unless @bound + write("#{idt}#{fvar} = #{func};\n#{idt}#{o[:return] ? 'return ' : ''}(function#{name_part}() {\n#{idt(1)}return #{fvar}.apply(__this, arguments);\n#{idt}});") end end @@ -726,7 +737,7 @@ def compile_node(o) body = Expressions.wrap(IfNode.new(@filter, body)) end if @object - o[:scope].top_level_assign("__hasProp", "Object.prototype.hasOwnProperty") + o[:scope].assign("__hasProp", "Object.prototype.hasOwnProperty", true) body = Expressions.wrap(IfNode.new( CallNode.new(ValueNode.new(LiteralNode.wrap("__hasProp"), [AccessorNode.new(Value.new('call'))]), [LiteralNode.wrap(svar), LiteralNode.wrap(ivar)]), Expressions.wrap(body), diff --git a/lib/coffee_script/rewriter.rb b/lib/coffee_script/rewriter.rb index 73d1106cfd..9635ed183e 100644 --- a/lib/coffee_script/rewriter.rb +++ b/lib/coffee_script/rewriter.rb @@ -26,7 +26,7 @@ class Rewriter # Single-line flavors of block expressions that have unclosed endings. # The grammar can't disambiguate them, so we insert the implicit indentation. - SINGLE_LINERS = [:ELSE, "=>", :TRY, :FINALLY, :THEN] + SINGLE_LINERS = [:ELSE, "=>", "==>", :TRY, :FINALLY, :THEN] SINGLE_CLOSERS = ["\n", :CATCH, :FINALLY, :ELSE, :OUTDENT, :LEADING_WHEN] # Rewrite the token stream in multiple passes, one logical filter at diff --git a/lib/coffee_script/scope.rb b/lib/coffee_script/scope.rb index a15865e6f5..2b34cea913 100644 --- a/lib/coffee_script/scope.rb +++ b/lib/coffee_script/scope.rb @@ -47,10 +47,10 @@ def free_variable @temp_variable.dup end - # Ensure that an assignment is made at the top-level scope. - # Takes two strings. - def top_level_assign(name, value) - return @parent.top_level_assign(name, value) if @parent + # Ensure that an assignment is made at the top of scope (or top-level + # scope, if requested). + def assign(name, value, top=false) + return @parent.assign(name, value, top) if top && @parent @variables[name.to_sym] = Value.new(value) end diff --git a/lib/coffee_script/value.rb b/lib/coffee_script/value.rb index bfd448ab2a..483ff8f8d3 100644 --- a/lib/coffee_script/value.rb +++ b/lib/coffee_script/value.rb @@ -41,6 +41,10 @@ def eql?(other) def hash @value.hash end + + def match(regex) + @value.match(regex) + end end end \ No newline at end of file diff --git a/test/fixtures/execution/test_functions.coffee b/test/fixtures/execution/test_functions.coffee new file mode 100644 index 0000000000..6a3aeb798f --- /dev/null +++ b/test/fixtures/execution/test_functions.coffee @@ -0,0 +1,22 @@ +x: 1 +y: {} +y.x: => 3 + +print(x is 1) +print(typeof(y.x) is 'function') +print(y.x() is 3) +print(y.x.name is 'x') + + +obj: { + name: "Fred" + + bound: => + (==> print(this.name is "Fred"))() + + unbound: => + (=> print(!this.name?))() +} + +obj.unbound() +obj.bound() \ No newline at end of file diff --git a/test/fixtures/execution/test_named_functions.coffee b/test/fixtures/execution/test_named_functions.coffee deleted file mode 100644 index 4fb9998b49..0000000000 --- a/test/fixtures/execution/test_named_functions.coffee +++ /dev/null @@ -1,8 +0,0 @@ -x: 1 -y: {} -y.x: => 3 - -print(x is 1) -print(typeof(y.x) is 'function') -print(y.x() is 3) -print(y.x.name is 'x') \ No newline at end of file From bb9fdd3015d643179c216bd3a6ef1ba0d623aeb9 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jan 2010 21:27:22 -0500 Subject: [PATCH 13/16] while loops can now be used as expressions -- they return an array containing the computed result of each iteration. --- examples/documents.coffee | 6 ++-- lib/coffee_script/grammar.y | 1 + lib/coffee_script/nodes.rb | 30 +++++++++++++++---- .../fixtures/execution/test_everything.coffee | 2 +- test/fixtures/execution/test_while.coffee | 17 +++++++++++ 5 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/execution/test_while.coffee diff --git a/examples/documents.coffee b/examples/documents.coffee index 439dd4ca56..31d38d8ed4 100644 --- a/examples/documents.coffee +++ b/examples/documents.coffee @@ -15,8 +15,8 @@ dc.model.Document: dc.Model.extend({ # document by binding to Metadata, instead of on-the-fly. metadata: => docId: this.id - _.select(Metadata.models(), (meta => - _.any(meta.get('instances'), instance => + _.select(Metadata.models(), (meta => + _.any(meta.get('instances'), instance => instance.document_id is docId))) bookmark: pageNumber => @@ -60,7 +60,7 @@ dc.model.DocumentSet: dc.model.RESTfulSet.extend({ # change their selected state. _onModelEvent: e, model => this.base(e, model) - fire: e == dc.Model.CHANGED and model.hasChanged('selected') + fire: e is dc.Model.CHANGED and model.hasChanged('selected') if fire then _.defer(_(this.fire).bind(this, this.SELECTION_CHANGED, this)) }) diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index f6bf246646..ea635e9663 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -353,6 +353,7 @@ rule While: WHILE Expression Block { result = WhileNode.new(val[1], val[2]) } | WHILE Expression { result = WhileNode.new(val[1], nil) } + | Expression WHILE Expression { result = WhileNode.new(val[2], Expressions.wrap(val[0])) } ; # Array comprehensions, including guard and current index. diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 0772add883..acc42eb683 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -36,9 +36,9 @@ def write(code) def compile(o={}) @options = o.dup @indent = o[:indent] - top = self.is_a?(ForNode) ? @options[:top] : @options.delete(:top) - closure = statement? && !statement_only? && !top && !@options[:return] - closure ? compile_closure(@options) : compile_node(@options) + top = self.top_sensitive? ? @options[:top] : @options.delete(:top) + closure = statement? && !statement_only? && !top && !@options[:return] + closure ? compile_closure(@options) : compile_node(@options) end def compile_closure(o={}) @@ -56,6 +56,7 @@ def idt(tabs=0) def unwrap; self; end def statement?; false; end def statement_only?; false; end + def top_sensitive?; false; end end # A collection of nodes, each one representing an expression. @@ -668,14 +669,27 @@ def initialize(condition, body) @condition, @body = condition, body end + def top_sensitive? + true + end + def compile_node(o) returns = o.delete(:return) + top = o.delete(:top) && !returns o[:indent] = idt(1) o[:top] = true cond = @condition.compile(o) - post = returns ? "\n#{idt}return null;" : '' - return write("#{idt}while (#{cond}) null;#{post}") if @body.nil? - write("#{idt}while (#{cond}) {\n#{@body.compile(o)}\n#{idt}}#{post}") + set = '' + if !top + rvar = o[:scope].free_variable + set = "#{idt}#{rvar} = [];\n" + @body = Expressions.wrap(CallNode.new( + ValueNode.new(LiteralNode.new(rvar), [AccessorNode.new('push')]), [@body.unwrap] + )) + end + post = returns ? "\n#{idt}return #{rvar};" : '' + return write("#{set}#{idt}while (#{cond}) null;#{post}") if @body.nil? + write("#{set}#{idt}while (#{cond}) {\n#{@body.compile(o)}\n#{idt}}#{post}") end end @@ -697,6 +711,10 @@ def initialize(body, source, name, index=nil) @name, @index = @index, @name if @object end + def top_sensitive? + true + end + def compile_node(o) top_level = o.delete(:top) && !o[:return] range = @source.is_a?(ValueNode) && @source.base.is_a?(RangeNode) && @source.properties.empty? diff --git a/test/fixtures/execution/test_everything.coffee b/test/fixtures/execution/test_everything.coffee index 87d444925c..a1ae6b8ffa 100644 --- a/test/fixtures/execution/test_everything.coffee +++ b/test/fixtures/execution/test_everything.coffee @@ -26,4 +26,4 @@ func: => c.single: c.list[1..1][0] -print(func() == '-') +print(func() is '-') diff --git a/test/fixtures/execution/test_while.coffee b/test/fixtures/execution/test_while.coffee new file mode 100644 index 0000000000..16972f1fe4 --- /dev/null +++ b/test/fixtures/execution/test_while.coffee @@ -0,0 +1,17 @@ +i: 100 +while i -= 1 + +print(i is 0) + + +i: 5 +list: while i -= 1 + i * 2 + +print(list.join(' ') is "8 6 4 2") + + +i: 5 +list: (i * 3 while i -= 1) + +print(list.join(' ') is "12 9 6 3") \ No newline at end of file From 2d206e7b60f08acb628fb60aaccbbe429e18b476 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jan 2010 21:33:46 -0500 Subject: [PATCH 14/16] pulling out pushes into a pushnode --- lib/coffee_script/nodes.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index acc42eb683..ddb52b846a 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -658,6 +658,17 @@ def compile_node(o) 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) + Expressions.wrap(CallNode.new( + ValueNode.new(LiteralNode.new(array), [AccessorNode.new('push')]), + [expressions.unwrap] + )) + 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 @@ -683,9 +694,7 @@ def compile_node(o) if !top rvar = o[:scope].free_variable set = "#{idt}#{rvar} = [];\n" - @body = Expressions.wrap(CallNode.new( - ValueNode.new(LiteralNode.new(rvar), [AccessorNode.new('push')]), [@body.unwrap] - )) + @body = PushNode.wrap(rvar, @body) end post = returns ? "\n#{idt}return #{rvar};" : '' return write("#{set}#{idt}while (#{cond}) null;#{post}") if @body.nil? @@ -743,9 +752,7 @@ def compile_node(o) if top_level body = Expressions.wrap(body) else - body = Expressions.wrap(CallNode.new( - ValueNode.new(LiteralNode.new(rvar), [AccessorNode.new('push')]), [body.unwrap] - )) + body = PushNode.wrap(rvar, body) end if o[:return] return_result = "return #{return_result}" if o[:return] From ed8a54995d39552290cfe668ae676a3e2e7e4286 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jan 2010 22:25:58 -0500 Subject: [PATCH 15/16] with splats allowed in destructuring assignment --- lib/coffee_script/nodes.rb | 13 ++++++++++--- .../test_destructuring_assignment.coffee | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index ddb52b846a..234560dae8 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -482,9 +482,12 @@ def compile_pattern_match(o) @variable.base.objects.each_with_index do |obj, i| obj, i = obj.value, obj.variable.base if @variable.object? access_class = @variable.array? ? IndexNode : AccessorNode - assigns << AssignNode.new( - obj, ValueNode.new(Value.new(val_var), [access_class.new(Value.new(i.to_s))]) - ).compile(o) + if obj.is_a?(SplatNode) + val = LiteralNode.wrap(obj.compile_value(o, val_var, @variable.base.objects.index(obj))) + else + val = ValueNode.new(Value.new(val_var), [access_class.new(Value.new(i.to_s))]) + end + assigns << AssignNode.new(obj, val).compile(o) end write(assigns.join("\n")) end @@ -609,6 +612,10 @@ def compile_arg(o) @name.compile(o) end + def compile_value(o, name, index) + "Array.prototype.slice.call(#{name}, #{index})" + end + end # An object literal. diff --git a/test/fixtures/execution/test_destructuring_assignment.coffee b/test/fixtures/execution/test_destructuring_assignment.coffee index 6998a700df..aba854a87a 100644 --- a/test/fixtures/execution/test_destructuring_assignment.coffee +++ b/test/fixtures/execution/test_destructuring_assignment.coffee @@ -43,4 +43,20 @@ person: { {name: a, family: {brother: {addresses: [one, {city: b}]}}}: person print(a is "Bob") -print(b is "Moquasset NY, 10021") \ No newline at end of file +print(b is "Moquasset NY, 10021") + + +test: { + person: { + address: [ + "------" + "Street 101" + "Apt 101" + "City 101" + ] + } +} + +{person: {address: [ignore, addr...]}}: test + +print(addr.join(', ') is "Street 101, Apt 101, City 101") \ No newline at end of file From e77e5206079efc1b74a32a0fcedfe663436a5b2a Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jan 2010 23:24:45 -0500 Subject: [PATCH 16/16] CoffeeScript 0.2.5 is on the books --- coffee-script.gemspec | 4 +- documentation/coffee/long_arrow.coffee | 6 + documentation/coffee/switch.coffee | 9 +- documentation/coffee/while.coffee | 11 +- documentation/index.html.erb | 37 +++- documentation/js/array_comprehensions.js | 6 +- documentation/js/expressions_comprehension.js | 3 +- documentation/js/long_arrow.js | 20 +++ documentation/js/object_comprehensions.js | 3 +- documentation/js/overview.js | 2 +- documentation/js/switch.js | 12 +- documentation/js/while.js | 23 ++- index.html | 158 ++++++++++++++---- lib/coffee-script.rb | 2 +- lib/coffee_script/lexer.rb | 11 +- lib/coffee_script/nodes.rb | 2 +- package.json | 2 +- 17 files changed, 239 insertions(+), 72 deletions(-) create mode 100644 documentation/coffee/long_arrow.coffee create mode 100644 documentation/js/long_arrow.js diff --git a/coffee-script.gemspec b/coffee-script.gemspec index 8a4aecc4ef..acbab9a1cc 100644 --- a/coffee-script.gemspec +++ b/coffee-script.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.name = 'coffee-script' - s.version = '0.2.4' # Keep version in sync with coffee-script.rb - s.date = '2010-1-12' + s.version = '0.2.5' # Keep version in sync with coffee-script.rb + s.date = '2010-1-13' s.homepage = "http://jashkenas.github.com/coffee-script/" s.summary = "The CoffeeScript Compiler" diff --git a/documentation/coffee/long_arrow.coffee b/documentation/coffee/long_arrow.coffee new file mode 100644 index 0000000000..86c64e9be4 --- /dev/null +++ b/documentation/coffee/long_arrow.coffee @@ -0,0 +1,6 @@ +Account: customer, cart => + this.customer: customer + this.cart: cart + + $('.shopping_cart').bind('click') event ==> + this.customer.purchase(this.cart) \ No newline at end of file diff --git a/documentation/coffee/switch.coffee b/documentation/coffee/switch.coffee index 7d9605e610..3478883d30 100644 --- a/documentation/coffee/switch.coffee +++ b/documentation/coffee/switch.coffee @@ -1,9 +1,10 @@ switch day - when "Tuesday" then eat_breakfast() - when "Wednesday" then go_to_the_park() - when "Saturday" + when "Mon" then go_to_work() + when "Tue" then go_to_the_park() + when "Thu" then go_ice_fishing() + when "Fri", "Sat" if day is bingo_day go_to_bingo() go_dancing() - when "Sunday" then go_to_church() + when "Sun" then go_to_church() else go_to_work() \ No newline at end of file diff --git a/documentation/coffee/while.coffee b/documentation/coffee/while.coffee index 3ca4a9845b..625e6ed6a1 100644 --- a/documentation/coffee/while.coffee +++ b/documentation/coffee/while.coffee @@ -1,5 +1,8 @@ -while demand > supply - sell() - restock() +if this.studying_economics + while supply > demand then buy() + while supply < demand then sell() -while supply > demand then buy() \ No newline at end of file +num: 6 +lyrics: while num -= 1 + num + " little monkeys, jumping on the bed. + One fell out and bumped his head." diff --git a/documentation/index.html.erb b/documentation/index.html.erb index f2ae351358..0927d89f60 100644 --- a/documentation/index.html.erb +++ b/documentation/index.html.erb @@ -51,7 +51,7 @@

Latest Version: - 0.2.4 + 0.2.5

Table of Contents

@@ -76,6 +76,7 @@ Inheritance, and Calling Super from a Subclass
Blocks
Pattern Matching
+ Function Binding
Embedded JavaScript
Switch/When/Else
Try/Catch/Finally
@@ -387,9 +388,12 @@ coffee --print app/scripts/*.coffee > concatenation.js

While Loops - The only low-level loop that CoffeeScript provides is the while loop. + The only low-level loop that CoffeeScript provides is the while loop. The + main difference from JavaScript is that the while loop can be used + as an expression, returning an array containing the result of each iteration + through the loop.

- <%= code_for('while') %> + <%= code_for('while', 'lyrics.join("\n")') %>

Other JavaScript loops, such as for loops and do-while loops can be mimicked by variations on while, but the hope is that you @@ -528,6 +532,17 @@ coffee --print app/scripts/*.coffee > concatenation.js

<%= code_for('object_extraction', 'poet + " — " + street') %> +

+ Function binding + The long arrow ==> can be used to both define a function, and to bind + it to the current value of this, right on the spot. This is helpful + when using callback-based libraries like Prototype or jQuery, for creating + iterator functions to pass to each, or event-handler functions + to use with bind. Functions created with the long arrow are able to access + properties of the this where they're defined. +

+ <%= code_for('long_arrow') %> +

Embedded JavaScript Hopefully, you'll never need to use it, but if you ever need to intersperse @@ -546,6 +561,11 @@ coffee --print app/scripts/*.coffee > concatenation.js in a returnable, assignable expression. The format is: switch condition, when clauses, else the default case.

+

+ As in Ruby, switch statements in CoffeeScript can take multiple + values for each when clause. If any of the values match, the clause + runs. +

<%= code_for('switch') %>

@@ -624,7 +644,16 @@ coffee --print app/scripts/*.coffee > concatenation.js

Change Log

- + +

+ 0.2.5 + The conditions in switch statements can now take multiple values at once — + If any of them are true, the case will run. Added the long arrow ==>, + which defines and immediately binds a function to this. While loops can + now be used as expressions, in the same way that comprehensions can. Splats + can be used within pattern matches to soak up the rest of an array. +

+

0.2.4 Added ECMAScript Harmony style destructuring assignment, for dealing with diff --git a/documentation/js/array_comprehensions.js b/documentation/js/array_comprehensions.js index fe7d5db668..fae31ce941 100644 --- a/documentation/js/array_comprehensions.js +++ b/documentation/js/array_comprehensions.js @@ -3,7 +3,7 @@ // Eat lunch. lunch = (function() { __a = []; __b = ['toast', 'cheese', 'wine']; - for (__c=0; __c<__b.length; __c++) { + for (__c = 0; __c < __b.length; __c++) { food = __b[__c]; __a.push(eat(food)); } @@ -11,10 +11,10 @@ })(); // Naive collision detection. __d = asteroids; - for (__e=0; __e<__d.length; __e++) { + for (__e = 0; __e < __d.length; __e++) { roid = __d[__e]; __f = asteroids; - for (__g=0; __g<__f.length; __g++) { + for (__g = 0; __g < __f.length; __g++) { roid2 = __f[__g]; if (roid !== roid2) { if (roid.overlaps(roid2)) { diff --git a/documentation/js/expressions_comprehension.js b/documentation/js/expressions_comprehension.js index 2d159d9f4f..294c3db4ed 100644 --- a/documentation/js/expressions_comprehension.js +++ b/documentation/js/expressions_comprehension.js @@ -1,10 +1,11 @@ (function(){ var __a, __b, globals, name; + var __hasProp = Object.prototype.hasOwnProperty; // The first ten global properties. globals = ((function() { __a = []; __b = window; for (name in __b) { - if (__b.hasOwnProperty(name)) { + if (__hasProp.call(__b, name)) { __a.push(name); } } diff --git a/documentation/js/long_arrow.js b/documentation/js/long_arrow.js new file mode 100644 index 0000000000..8e60b143e1 --- /dev/null +++ b/documentation/js/long_arrow.js @@ -0,0 +1,20 @@ +(function(){ + var Account; + Account = function Account(customer, cart) { + var __a, __b; + var __this = this; + this.customer = customer; + this.cart = cart; + __a = $('.shopping_cart').bind('click', (function() { + __b = function(event) { + var __c; + __c = this.customer.purchase(this.cart); + return Account === this.constructor ? this : __c; + }; + return (function() { + return __b.apply(__this, arguments); + }); + })()); + return Account === this.constructor ? this : __a; + }; +})(); \ No newline at end of file diff --git a/documentation/js/object_comprehensions.js b/documentation/js/object_comprehensions.js index 81d94a417c..9c7d3783b9 100644 --- a/documentation/js/object_comprehensions.js +++ b/documentation/js/object_comprehensions.js @@ -1,5 +1,6 @@ (function(){ var __a, __b, age, ages, child, years_old; + var __hasProp = Object.prototype.hasOwnProperty; years_old = { max: 10, ida: 9, @@ -9,7 +10,7 @@ __a = []; __b = years_old; for (child in __b) { age = __b[child]; - if (__b.hasOwnProperty(child)) { + if (__hasProp.call(__b, child)) { __a.push(child + " is " + age); } } diff --git a/documentation/js/overview.js b/documentation/js/overview.js index 1e61b37199..2098e3a30f 100644 --- a/documentation/js/overview.js +++ b/documentation/js/overview.js @@ -34,7 +34,7 @@ // Array comprehensions: cubed_list = (function() { __a = []; __b = list; - for (__c=0; __c<__b.length; __c++) { + for (__c = 0; __c < __b.length; __c++) { num = __b[__c]; __a.push(math.cube(num)); } diff --git a/documentation/js/switch.js b/documentation/js/switch.js index 53406faae9..77e9a77d5b 100644 --- a/documentation/js/switch.js +++ b/documentation/js/switch.js @@ -1,14 +1,16 @@ (function(){ - if (day === "Tuesday") { - eat_breakfast(); - } else if (day === "Wednesday") { + if (day === "Mon") { + go_to_work(); + } else if (day === "Tue") { go_to_the_park(); - } else if (day === "Saturday") { + } else if (day === "Thu") { + go_ice_fishing(); + } else if (day === "Fri" || day === "Sat") { if (day === bingo_day) { go_to_bingo(); go_dancing(); } - } else if (day === "Sunday") { + } else if (day === "Sun") { go_to_church(); } else { go_to_work(); diff --git a/documentation/js/while.js b/documentation/js/while.js index cc83571984..0ac87e11a4 100644 --- a/documentation/js/while.js +++ b/documentation/js/while.js @@ -1,9 +1,20 @@ (function(){ - while (demand > supply) { - sell(); - restock(); - } - while (supply > demand) { - buy(); + var __a, lyrics, num; + if (this.studying_economics) { + while (supply > demand) { + buy(); + } + while (supply < demand) { + sell(); + } } + num = 6; + lyrics = (function() { + __a = []; + while (num -= 1) { + __a.push(num + " little monkeys, jumping on the bed. \ +One fell out and bumped his head."); + } + return __a; + })(); })(); \ No newline at end of file diff --git a/index.html b/index.html index dd74befcc5..4a039662a1 100644 --- a/index.html +++ b/index.html @@ -37,7 +37,7 @@

CoffeeScript

Latest Version: - 0.2.4 + 0.2.5

Table of Contents

@@ -62,6 +62,7 @@

Table of Contents

Inheritance, and Calling Super from a Subclass
Blocks
Pattern Matching
+ Function Binding
Embedded JavaScript
Switch/When/Else
Try/Catch/Finally
@@ -139,7 +140,7 @@

Mini Overview

// Array comprehensions: cubed_list = (function() { __a = []; __b = list; - for (__c=0; __c<__b.length; __c++) { + for (__c = 0; __c < __b.length; __c++) { num = __b[__c]; __a.push(math.cube(num)); } @@ -180,7 +181,7 @@

Mini Overview

// Array comprehensions: cubed_list = (function() { __a = []; __b = list; - for (__c=0; __c<__b.length; __c++) { + for (__c = 0; __c < __b.length; __c++) { num = __b[__c]; __a.push(math.cube(num)); } @@ -667,21 +668,56 @@

Language Reference

While Loops - The only low-level loop that CoffeeScript provides is the while loop. -

-
while demand > supply
-  sell()
-  restock()
-
-while supply > demand then buy()
-
while (demand > supply) {
-  sell();
-  restock();
+      The only low-level loop that CoffeeScript provides is the while loop. The
+      main difference from JavaScript is that the while loop can be used
+      as an expression, returning an array containing the result of each iteration
+      through the loop.
+    

+
if this.studying_economics
+  while supply > demand then buy()
+  while supply < demand then sell()
+
+num: 6
+lyrics: while num -= 1
+  num + " little monkeys, jumping on the bed.
+    One fell out and bumped his head."
+
var __a, lyrics, num;
+if (this.studying_economics) {
+  while (supply > demand) {
+    buy();
+  }
+  while (supply < demand) {
+    sell();
+  }
 }
-while (supply > demand) {
-  buy();
+num = 6;
+lyrics = (function() {
+  __a = [];
+  while (num -= 1) {
+    __a.push(num + " little monkeys, jumping on the bed. \
+One fell out and bumped his head.");
+  }
+  return __a;
+})();
+
+num = 6; +lyrics = (function() { + __a = []; + while (num -= 1) { + __a.push(num + " little monkeys, jumping on the bed. \ +One fell out and bumped his head."); + } + return __a; +})(); +;alert(lyrics.join("\n"));'>run: lyrics.join("\n")

Other JavaScript loops, such as for loops and do-while loops can be mimicked by variations on while, but the hope is that you @@ -709,7 +745,7 @@

Language Reference

// Eat lunch. lunch = (function() { __a = []; __b = ['toast', 'cheese', 'wine']; - for (__c=0; __c<__b.length; __c++) { + for (__c = 0; __c < __b.length; __c++) { food = __b[__c]; __a.push(eat(food)); } @@ -717,10 +753,10 @@

Language Reference

})(); // Naive collision detection. __d = asteroids; -for (__e=0; __e<__d.length; __e++) { +for (__e = 0; __e < __d.length; __e++) { roid = __d[__e]; __f = asteroids; - for (__g=0; __g<__f.length; __g++) { + for (__g = 0; __g < __f.length; __g++) { roid2 = __f[__g]; if (roid !== roid2) { if (roid.overlaps(roid2)) { @@ -791,6 +827,7 @@

Language Reference

ages: for child, age of years_old child + " is " + age
var __a, __b, age, ages, child, years_old;
+var __hasProp = Object.prototype.hasOwnProperty;
 years_old = {
   max: 10,
   ida: 9,
@@ -800,13 +837,14 @@ 

Language Reference

__a = []; __b = years_old; for (child in __b) { age = __b[child]; - if (__b.hasOwnProperty(child)) { + if (__hasProp.call(__b, child)) { __a.push(child + " is " + age); } } return __a; })();

+

+ Function binding + The long arrow ==> can be used to both define a function, and to bind + it to the current value of this, right on the spot. This is helpful + when using callback-based libraries like Prototype or jQuery, for creating + iterator functions to pass to each, or event-handler functions + to use with bind. Functions created with the long arrow are able to access + properties of the this where they're defined. +

+
Account: customer, cart =>
+  this.customer: customer
+  this.cart: cart
+
+  $('.shopping_cart').bind('click') event ==>
+    this.customer.purchase(this.cart)
+
var Account;
+Account = function Account(customer, cart) {
+  var __a, __b;
+  var __this = this;
+  this.customer = customer;
+  this.cart = cart;
+  __a = $('.shopping_cart').bind('click', (function() {
+    __b = function(event) {
+      var __c;
+      __c = this.customer.purchase(this.cart);
+      return Account === this.constructor ? this : __c;
+    };
+    return (function() {
+      return __b.apply(__this, arguments);
+    });
+  })());
+  return Account === this.constructor ? this : __a;
+};
+

+

Embedded JavaScript Hopefully, you'll never need to use it, but if you ever need to intersperse @@ -1261,25 +1336,33 @@

Language Reference

in a returnable, assignable expression. The format is: switch condition, when clauses, else the default case.

+

+ As in Ruby, switch statements in CoffeeScript can take multiple + values for each when clause. If any of the values match, the clause + runs. +

switch day
-  when "Tuesday"   then eat_breakfast()
-  when "Wednesday" then go_to_the_park()
-  when "Saturday"
+  when "Mon" then go_to_work()
+  when "Tue" then go_to_the_park()
+  when "Thu" then go_ice_fishing()
+  when "Fri", "Sat"
     if day is bingo_day
       go_to_bingo()
       go_dancing()
-  when "Sunday"    then go_to_church()
+  when "Sun" then go_to_church()
   else go_to_work()
-
if (day === "Tuesday") {
-  eat_breakfast();
-} else if (day === "Wednesday") {
+
if (day === "Mon") {
+  go_to_work();
+} else if (day === "Tue") {
   go_to_the_park();
-} else if (day === "Saturday") {
+} else if (day === "Thu") {
+  go_ice_fishing();
+} else if (day === "Fri" || day === "Sat") {
   if (day === bingo_day) {
     go_to_bingo();
     go_dancing();
   }
-} else if (day === "Sunday") {
+} else if (day === "Sun") {
   go_to_church();
 } else {
   go_to_work();
@@ -1406,7 +1489,16 @@ 

Contributing

Change Log

- + +

+ 0.2.5 + The conditions in switch statements can now take multiple values at once — + If any of them are true, the case will run. Added the long arrow ==>, + which defines and immediately binds a function to this. While loops can + now be used as expressions, in the same way that comprehensions can. Splats + can be used within pattern matches to soak up the rest of an array. +

+

0.2.4 Added ECMAScript Harmony style destructuring assignment, for dealing with diff --git a/lib/coffee-script.rb b/lib/coffee-script.rb index 3fc2d6ad32..8e92bc9f02 100644 --- a/lib/coffee-script.rb +++ b/lib/coffee-script.rb @@ -10,7 +10,7 @@ # Namespace for all CoffeeScript internal classes. module CoffeeScript - VERSION = '0.2.4' # Keep in sync with the gemspec. + VERSION = '0.2.5' # Keep in sync with the gemspec. # Compile a script (String or IO) to JavaScript. def self.compile(script, options={}) diff --git a/lib/coffee_script/lexer.rb b/lib/coffee_script/lexer.rb index 1667e2991b..b27fb72529 100644 --- a/lib/coffee_script/lexer.rb +++ b/lib/coffee_script/lexer.rb @@ -34,11 +34,12 @@ class Lexer ASSIGNMENT = /\A(:|=)\Z/ # Token cleaning regexes. - JS_CLEANER = /(\A`|`\Z)/ - MULTILINER = /\n/ + JS_CLEANER = /(\A`|`\Z)/ + MULTILINER = /\n/ + STRING_NEWLINES = /\n\s*/ COMMENT_CLEANER = /(^\s*#|\n\s*$)/ - NO_NEWLINE = /\A([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)\Z/ - HEREDOC_INDENT = /^\s+/ + NO_NEWLINE = /\A([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)\Z/ + HEREDOC_INDENT = /^\s+/ # Tokens which a regular expression will never immediately follow, but which # a division operator might. @@ -106,7 +107,7 @@ def number_token # Matches strings, including multi-line strings. def string_token return false unless string = @chunk[STRING, 1] - escaped = string.gsub(MULTILINER, " \\\n") + escaped = string.gsub(STRING_NEWLINES, " \\\n") token(:STRING, escaped) @line += string.count("\n") @i += string.length diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 234560dae8..2ea590581a 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -750,7 +750,7 @@ def compile_node(o) else index_var = nil source_part = "#{svar} = #{source.compile(o)};\n#{idt}" - for_part = @object ? "#{ivar} in #{svar}" : "#{ivar}=0; #{ivar}<#{svar}.length; #{ivar}++" + for_part = @object ? "#{ivar} in #{svar}" : "#{ivar} = 0; #{ivar} < #{svar}.length; #{ivar}++" var_part = @name ? "#{body_dent}#{@name} = #{svar}[#{ivar}];\n" : '' end body = @body diff --git a/package.json b/package.json index 0d1478fa3f..3c86562875 100644 --- a/package.json +++ b/package.json @@ -5,5 +5,5 @@ "description": "Unfancy JavaScript", "keywords": ["javascript", "language"], "author": "Jeremy Ashkenas", - "version": "0.2.4" + "version": "0.2.5" }