diff --git a/lib/coffee_script/lexer.rb b/lib/coffee_script/lexer.rb index dd0f50a6..d000297a 100644 --- a/lib/coffee_script/lexer.rb +++ b/lib/coffee_script/lexer.rb @@ -1,7 +1,11 @@ module CoffeeScript + # The lexer reads a stream of CoffeeScript and divvys it up into tagged + # tokens. A minor bit of the ambiguity in the grammar has been avoided by + # pushing some extra smarts into the Lexer. class Lexer + # The list of keywords passed verbatim to the parser. KEYWORDS = ["if", "else", "then", "unless", "true", "false", "null", "and", "or", "is", "aint", "not", @@ -13,6 +17,7 @@ module CoffeeScript "super", "delete"] + # Token matching regexes. 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 @@ -24,19 +29,22 @@ module CoffeeScript CODE = /\A(=>)/ REGEX = /\A(\/(.*?)[^\\]\/[imgy]{0,4})/ + # Token cleaning regexes. JS_CLEANER = /(\A`|`\Z)/ MULTILINER = /[\r\n]/ + # Tokens that always constitute the start of an expression. EXP_START = ['{', '(', '['] + + # Tokens that always constitute the end of an expression. EXP_END = ['}', ')', ']'] - # This is how to implement a very simple scanner. - # Scan one caracter at the time until you find something to parse. + # 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. - @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. + @tokens = [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value] while @i < @code.length @chunk = @code[@i..-1] extract_next_token @@ -44,6 +52,8 @@ module CoffeeScript @tokens end + # At every position, run this list of match attempts, short-circuiting if + # any of them succeed. def extract_next_token return if identifier_token return if number_token @@ -55,7 +65,7 @@ module CoffeeScript return literal_token end - # Matching if, print, method names, etc. + # Matches identifying literals: variables, keywords, method names, etc. def identifier_token return false unless identifier = @chunk[IDENTIFIER, 1] # Keywords are special identifiers tagged with their own name, 'if' will result @@ -66,12 +76,14 @@ module CoffeeScript @i += identifier.length end + # Matches numbers, including decimals, hex, and exponential notation. def number_token return false unless number = @chunk[NUMBER, 1] token(:NUMBER, number) @i += number.length end + # Matches strings, including multi-line strings. def string_token return false unless string = @chunk[STRING, 1] escaped = string.gsub(MULTILINER) do |match| @@ -82,24 +94,27 @@ module CoffeeScript @i += string.length end + # Matches interpolated JavaScript. def js_token return false unless script = @chunk[JS, 1] token(:JS, script.gsub(JS_CLEANER, '')) @i += script.length end + # Matches regular expression literals. def regex_token return false unless regex = @chunk[REGEX, 1] token(:REGEX, regex) @i += regex.length end + # Matches and consumes comments. def remove_comment return false unless comment = @chunk[COMMENT, 1] @i += comment.length end - # Ignore whitespace + # Matches and consumes non-meaningful whitespace. def whitespace_token return false unless whitespace = @chunk[WHITESPACE, 1] @i += whitespace.length @@ -107,7 +122,7 @@ module CoffeeScript # We treat all other single characters as a token. Eg.: ( ) , . ! # Multi-character operators are also literal tokens, so that Racc can assign - # the proper order of operations. Multiple newlines get merged. + # the proper order of operations. Multiple newlines get merged together. def literal_token value = @chunk[NEWLINE, 1] if value @@ -124,16 +139,20 @@ module CoffeeScript @i += value.length end + # Add a token to the results, taking note of the line number for syntax + # errors later in the parse. def token(tag, value) @tokens << [tag, Value.new(value, @line)] end + # Peek at the previous token. def last_value @tokens.last && @tokens.last[1] end - # The main source of ambiguity in our grammar was Parameter lists (as opposed - # to argument lists in method calls). Tag parameter identifiers to avoid this. + # A source of ambiguity in our grammar was parameter lists in function + # definitions (as opposed to argument lists in function calls). Tag + # parameter identifiers in order to avoid this. def tag_parameters index = 0 loop do @@ -144,6 +163,7 @@ module CoffeeScript end end + # Consume and ignore newlines immediately after this point. def skip_following_newlines newlines = @code[(@i+1)..-1][NEWLINE, 1] if newlines @@ -152,6 +172,7 @@ module CoffeeScript end end + # Discard newlines immediately before this point. def remove_leading_newlines @tokens.pop if last_value == "\n" end diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index 05644abb..cfe38204 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -1,469 +1,473 @@ -class Node - # Tabs are two spaces for pretty-printing. - TAB = ' ' +module CoffeeScript - def flatten; self; end - def line_ending; ';'; end - def statement?; false; end - def custom_return?; false; end - def custom_assign?; false; end + class Node + # Tabs are two spaces for pretty-printing. + TAB = ' ' - def compile(indent='', scope=nil, opts={}); end -end + def flatten; self; end + def line_ending; ';'; end + def statement?; false; end + def custom_return?; false; end + def custom_assign?; false; end -# Collection of nodes each one representing an expression. -class Nodes < Node - attr_reader :nodes - - def self.wrap(node) - node.is_a?(Nodes) ? node : Nodes.new([node]) + def compile(indent='', scope=nil, opts={}); end end - def initialize(nodes) - @nodes = nodes - end + # Collection of nodes each one representing an expression. + class Nodes < Node + attr_reader :nodes - def <<(node) - @nodes << node - self - end + def self.wrap(node) + node.is_a?(Nodes) ? node : Nodes.new([node]) + end - def flatten - @nodes.length == 1 ? @nodes.first : self - end + def initialize(nodes) + @nodes = nodes + end - def begin_compile - "(function(){\n#{compile(TAB, Scope.new)}\n})();" - end + def <<(node) + @nodes << node + self + end - def statement? - true - end + def flatten + @nodes.length == 1 ? @nodes.first : self + end - # Fancy to handle pushing down returns recursively to the final lines of - # inner statements (to make expressions out of them). - def compile(indent='', scope=nil, opts={}) - return begin_compile unless scope - @nodes.map { |n| - if opts[:return] && n == @nodes.last - if n.statement? || n.custom_return? - "#{indent}#{n.compile(indent, scope, opts)}#{n.line_ending}" + def begin_compile + "(function(){\n#{compile(TAB, Scope.new)}\n})();" + end + + def statement? + true + end + + # Fancy to handle pushing down returns recursively to the final lines of + # inner statements (to make expressions out of them). + def compile(indent='', scope=nil, opts={}) + return begin_compile unless scope + @nodes.map { |n| + if opts[:return] && n == @nodes.last + if n.statement? || n.custom_return? + "#{indent}#{n.compile(indent, scope, opts)}#{n.line_ending}" + else + "#{indent}return #{n.compile(indent, scope, opts)}#{n.line_ending}" + end else - "#{indent}return #{n.compile(indent, scope, opts)}#{n.line_ending}" + "#{indent}#{n.compile(indent, scope)}#{n.line_ending}" end - else - "#{indent}#{n.compile(indent, scope)}#{n.line_ending}" + }.join("\n") + end + end + + # Literals are static values that have a Ruby representation, eg.: a string, a number, + # true, false, nil, etc. + class LiteralNode < Node + STATEMENTS = ['break', 'continue'] + + def initialize(value) + @value = value + end + + def statement? + STATEMENTS.include?(@value.to_s) + end + + def compile(indent, scope, opts={}) + @value.to_s + end + end + + class ReturnNode < Node + def initialize(expression) + @expression = expression + end + + def statement? + true + end + + def custom_return? + true + end + + def compile(indent, scope, opts={}) + return @expression.compile(indent, scope, opts.merge(:return => true)) if @expression.custom_return? + compiled = @expression.compile(indent, scope) + @expression.statement? ? "#{compiled}\n#{indent}return null" : "return #{compiled}" + end + end + + # Node of a method call or local variable access, can take any of these forms: + # + # method # this form can also be a local variable + # method(argument1, argument2) + # receiver.method + # receiver.method(argument1, argument2) + # + class CallNode < Node + LEADING_DOT = /\A\./ + + def initialize(variable, arguments=[]) + @variable, @arguments = variable, arguments + end + + def new_instance + @new = true + self + end + + def super? + @variable == :super + end + + def compile(indent, scope, opts={}) + args = @arguments.map{|a| a.compile(indent, scope, :no_paren => true) }.join(', ') + return compile_super(args, indent, scope, opts) if super? + prefix = @new ? "new " : '' + "#{prefix}#{@variable.compile(indent, scope)}(#{args})" + end + + def compile_super(args, indent, scope, opts) + methname = opts[:last_assign].sub(LEADING_DOT, '') + "this.constructor.prototype.#{methname}.call(this, #{args})" + end + end + + class ValueNode < Node + attr_reader :last + + def initialize(name, properties=[]) + @name, @properties = name, properties + end + + def <<(other) + @properties << other + self + end + + def properties? + return !@properties.empty? + end + + def compile(indent, scope, opts={}) + parts = [@name, @properties].flatten.map do |v| + v.respond_to?(:compile) ? v.compile(indent, scope) : v.to_s end - }.join("\n") - end -end - -# Literals are static values that have a Ruby representation, eg.: a string, a number, -# true, false, nil, etc. -class LiteralNode < Node - STATEMENTS = ['break', 'continue'] - - def initialize(value) - @value = value - end - - def statement? - STATEMENTS.include?(@value.to_s) - end - - def compile(indent, scope, opts={}) - @value.to_s - end -end - -class ReturnNode < Node - def initialize(expression) - @expression = expression - end - - def statement? - true - end - - def custom_return? - true - end - - def compile(indent, scope, opts={}) - return @expression.compile(indent, scope, opts.merge(:return => true)) if @expression.custom_return? - compiled = @expression.compile(indent, scope) - @expression.statement? ? "#{compiled}\n#{indent}return null" : "return #{compiled}" - end -end - -# Node of a method call or local variable access, can take any of these forms: -# -# method # this form can also be a local variable -# method(argument1, argument2) -# receiver.method -# receiver.method(argument1, argument2) -# -class CallNode < Node - LEADING_DOT = /\A\./ - - def initialize(variable, arguments=[]) - @variable, @arguments = variable, arguments - end - - def new_instance - @new = true - self - end - - def super? - @variable == :super - end - - def compile(indent, scope, opts={}) - args = @arguments.map{|a| a.compile(indent, scope, :no_paren => true) }.join(', ') - return compile_super(args, indent, scope, opts) if super? - prefix = @new ? "new " : '' - "#{prefix}#{@variable.compile(indent, scope)}(#{args})" - end - - def compile_super(args, indent, scope, opts) - methname = opts[:last_assign].sub(LEADING_DOT, '') - "this.constructor.prototype.#{methname}.call(this, #{args})" - end -end - -class ValueNode < Node - attr_reader :last - - def initialize(name, properties=[]) - @name, @properties = name, properties - end - - def <<(other) - @properties << other - self - end - - def properties? - return !@properties.empty? - end - - def compile(indent, scope, opts={}) - parts = [@name, @properties].flatten.map do |v| - v.respond_to?(:compile) ? v.compile(indent, scope) : v.to_s + @last = parts.last + parts.join('') end - @last = parts.last - parts.join('') - end -end - -class AccessorNode - def initialize(name) - @name = name end - def compile(indent, scope, opts={}) - ".#{@name}" - end -end - -class IndexNode - def initialize(index) - @index = index - end - - def compile(indent, scope, opts={}) - "[#{@index.compile(indent, scope)}]" - end -end - -class SliceNode - def initialize(from, to) - @from, @to = from, to - end - - def compile(indent, scope, opts={}) - ".slice(#{@from.compile(indent, scope, opts)}, #{@to.compile(indent, scope, opts)} + 1)" - end -end - -# Setting the value of a local variable. -class AssignNode < Node - def initialize(variable, value, context=nil) - @variable, @value, @context = variable, value, context - end - - def custom_return? - true - end - - def statement? - true - end - - def compile(indent, scope, opts={}) - name = @variable.compile(indent, scope) if @variable.respond_to?(:compile) - last = @variable.respond_to?(:last) ? @variable.last : name - opts = opts.merge({:assign => name, :last_assign => last}) - value = @value.compile(indent + TAB, scope, opts) - return "#{@variable}: #{value}" if @context == :object - return "#{name} = #{value}" if @variable.properties? - defined = scope.find(name) - postfix = !defined && opts[:return] ? ";\n#{indent}return #{name}" : '' - def_part = defined ? "" : "var #{name};\n#{indent}" - return def_part + @value.compile(indent, scope, opts) if @value.custom_assign? - def_part = defined ? name : "var #{name}" - "#{def_part} = #{@value.compile(indent, scope, opts)}#{postfix}" - end -end - -# Simple Arithmetic and logical operations -class OpNode < Node - CONVERSIONS = { - "==" => "===", - "!=" => "!==", - 'and' => '&&', - 'or' => '||', - 'is' => '===', - "aint" => "!==", - 'not' => '!', - } - CONDITIONALS = ['||=', '&&='] - - def initialize(operator, first, second=nil) - @first, @second = first, second - @operator = CONVERSIONS[operator] || operator - end - - def unary? - @second.nil? - end - - def compile(indent, scope, opts={}) - return compile_conditional(indent, scope) if CONDITIONALS.include?(@operator) - return compile_unary(indent, scope) if unary? - "#{@first.compile(indent, scope)} #{@operator} #{@second.compile(indent, scope)}" - end - - def compile_conditional(indent, scope) - first, second = @first.compile(indent, scope), @second.compile(indent, scope) - sym = @operator[0..1] - "#{first} = #{first} #{sym} #{second}" - end - - def compile_unary(indent, scope) - space = @operator == 'delete' ? ' ' : '' - "#{@operator}#{space}#{@first.compile(indent, scope)}" - end -end - -# Method definition. -class CodeNode < Node - def initialize(params, body) - @params = params - @body = body - end - - def compile(indent, scope, opts={}) - scope = Scope.new(scope) - @params.each {|id| scope.find(id) } - opts = opts.merge(:return => true) - code = @body.compile(indent + TAB, scope, opts) - "function(#{@params.join(', ')}) {\n#{code}\n#{indent}}" - end -end - -class ObjectNode < Node - def initialize(properties = []) - @properties = properties - end - - def compile(indent, scope, opts={}) - props = @properties.map {|p| indent + TAB + p.compile(indent, scope) }.join(",\n") - "{\n#{props}\n#{indent}}" - end -end - -class ArrayNode < Node - def initialize(objects=[]) - @objects = objects - end - - def compile(indent, scope, opts={}) - objects = @objects.map {|o| o.compile(indent, scope) }.join(', ') - "[#{objects}]" - end -end - -class WhileNode < Node - def initialize(condition, body) - @condition, @body = condition, body - end - - def line_ending - '' - end - - def statement? - true - end - - def compile(indent, scope, opts={}) - "while (#{@condition.compile(indent, scope, :no_paren => true)}) {\n#{@body.compile(indent + TAB, scope)}\n#{indent}}" - end -end - -class ForNode < Node - - def initialize(body, source, name, index=nil) - @body, @source, @name, @index = body, source, name, index - end - - def line_ending - '' - end - - def custom_return? - true - end - - def custom_assign? - true - end - - def statement? - true - end - - def compile(indent, scope, opts={}) - svar = scope.free_variable - ivar = scope.free_variable - lvar = scope.free_variable - name_part = scope.find(@name) ? @name : "var #{@name}" - index_name = @index ? (scope.find(@index) ? @index : "var #{@index}") : nil - source_part = "var #{svar} = #{@source.compile(indent, scope)};" - for_part = "var #{ivar}=0, #{lvar}=#{svar}.length; #{ivar}<#{lvar}; #{ivar}++" - var_part = "\n#{indent + TAB}#{name_part} = #{svar}[#{ivar}];\n" - index_part = @index ? "#{indent + TAB}#{index_name} = #{ivar};\n" : '' - - set_result = '' - save_result = '' - return_result = '' - if opts[:return] || opts[:assign] - rvar = scope.free_variable - set_result = "var #{rvar} = [];\n#{indent}" - 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}" + class AccessorNode + def initialize(name) + @name = name 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}" - end -end - -class TryNode < Node - def initialize(try, error, recovery, finally=nil) - @try, @error, @recovery, @finally = try, error, recovery, finally + def compile(indent, scope, opts={}) + ".#{@name}" + end end - def line_ending - '' + class IndexNode + def initialize(index) + @index = index + end + + def compile(indent, scope, opts={}) + "[#{@index.compile(indent, scope)}]" + end end - def statement? - true + class SliceNode + def initialize(from, to) + @from, @to = from, to + end + + def compile(indent, scope, opts={}) + ".slice(#{@from.compile(indent, scope, opts)}, #{@to.compile(indent, scope, opts)} + 1)" + end end - def compile(indent, scope, opts={}) - catch_part = @recovery && " catch (#{@error}) {\n#{@recovery.compile(indent + TAB, scope, opts)}\n#{indent}}" - finally_part = @finally && " finally {\n#{@finally.compile(indent + TAB, scope, opts)}\n#{indent}}" - "try {\n#{@try.compile(indent + TAB, scope, opts)}\n#{indent}}#{catch_part}#{finally_part}" - end -end + # Setting the value of a local variable. + class AssignNode < Node + def initialize(variable, value, context=nil) + @variable, @value, @context = variable, value, context + end -class ThrowNode < Node - def initialize(expression) - @expression = expression + def custom_return? + true + end + + def statement? + true + end + + def compile(indent, scope, opts={}) + name = @variable.compile(indent, scope) if @variable.respond_to?(:compile) + last = @variable.respond_to?(:last) ? @variable.last : name + opts = opts.merge({:assign => name, :last_assign => last}) + value = @value.compile(indent + TAB, scope, opts) + return "#{@variable}: #{value}" if @context == :object + return "#{name} = #{value}" if @variable.properties? + defined = scope.find(name) + postfix = !defined && opts[:return] ? ";\n#{indent}return #{name}" : '' + def_part = defined ? "" : "var #{name};\n#{indent}" + return def_part + @value.compile(indent, scope, opts) if @value.custom_assign? + def_part = defined ? name : "var #{name}" + "#{def_part} = #{@value.compile(indent, scope, opts)}#{postfix}" + end end - def statement? - true + # Simple Arithmetic and logical operations + class OpNode < Node + CONVERSIONS = { + "==" => "===", + "!=" => "!==", + 'and' => '&&', + 'or' => '||', + 'is' => '===', + "aint" => "!==", + 'not' => '!', + } + CONDITIONALS = ['||=', '&&='] + + def initialize(operator, first, second=nil) + @first, @second = first, second + @operator = CONVERSIONS[operator] || operator + end + + def unary? + @second.nil? + end + + def compile(indent, scope, opts={}) + return compile_conditional(indent, scope) if CONDITIONALS.include?(@operator) + return compile_unary(indent, scope) if unary? + "#{@first.compile(indent, scope)} #{@operator} #{@second.compile(indent, scope)}" + end + + def compile_conditional(indent, scope) + first, second = @first.compile(indent, scope), @second.compile(indent, scope) + sym = @operator[0..1] + "#{first} = #{first} #{sym} #{second}" + end + + def compile_unary(indent, scope) + space = @operator == 'delete' ? ' ' : '' + "#{@operator}#{space}#{@first.compile(indent, scope)}" + end end - def compile(indent, scope, opts={}) - "throw #{@expression.compile(indent, scope)}" - end -end + # Method definition. + class CodeNode < Node + def initialize(params, body) + @params = params + @body = body + end -class ParentheticalNode < Node - def initialize(expressions) - @expressions = expressions + def compile(indent, scope, opts={}) + scope = Scope.new(scope) + @params.each {|id| scope.find(id) } + opts = opts.merge(:return => true) + code = @body.compile(indent + TAB, scope, opts) + "function(#{@params.join(', ')}) {\n#{code}\n#{indent}}" + end end - def compile(indent, scope, opts={}) - compiled = @expressions.flatten.compile(indent, scope) - compiled = compiled[0...-1] if compiled[-1..-1] == ';' - opts[:no_paren] ? compiled : "(#{compiled})" - end -end + class ObjectNode < Node + def initialize(properties = []) + @properties = properties + end -# "if-else" control structure. Look at this node if you want to implement other control -# structures like while, for, loop, etc. -class IfNode < Node - def initialize(condition, body, else_body=nil, tag=nil) - @condition = condition - @body = body && body.flatten - @else_body = else_body && else_body.flatten - @condition = OpNode.new("!", @condition) if tag == :invert + def compile(indent, scope, opts={}) + props = @properties.map {|p| indent + TAB + p.compile(indent, scope) }.join(",\n") + "{\n#{props}\n#{indent}}" + end end - def <<(else_body) - eb = else_body.flatten - @else_body ? @else_body << eb : @else_body = eb - self + class ArrayNode < Node + def initialize(objects=[]) + @objects = objects + end + + def compile(indent, scope, opts={}) + objects = @objects.map {|o| o.compile(indent, scope) }.join(', ') + "[#{objects}]" + end end - # Rewrite a chain of IfNodes with their switch condition for equality. - def rewrite_condition(expression) - @condition = OpNode.new("is", expression, @condition) - @else_body.rewrite_condition(expression) if chain? - self + class WhileNode < Node + def initialize(condition, body) + @condition, @body = condition, body + end + + def line_ending + '' + end + + def statement? + true + end + + def compile(indent, scope, opts={}) + "while (#{@condition.compile(indent, scope, :no_paren => true)}) {\n#{@body.compile(indent + TAB, scope)}\n#{indent}}" + end end - # Rewrite a chain of IfNodes to add a default case as the final else. - def add_else(expressions) - chain? ? @else_body.add_else(expressions) : @else_body = expressions - self + class ForNode < Node + + def initialize(body, source, name, index=nil) + @body, @source, @name, @index = body, source, name, index + end + + def line_ending + '' + end + + def custom_return? + true + end + + def custom_assign? + true + end + + def statement? + true + end + + def compile(indent, scope, opts={}) + svar = scope.free_variable + ivar = scope.free_variable + lvar = scope.free_variable + name_part = scope.find(@name) ? @name : "var #{@name}" + index_name = @index ? (scope.find(@index) ? @index : "var #{@index}") : nil + source_part = "var #{svar} = #{@source.compile(indent, scope)};" + for_part = "var #{ivar}=0, #{lvar}=#{svar}.length; #{ivar}<#{lvar}; #{ivar}++" + var_part = "\n#{indent + TAB}#{name_part} = #{svar}[#{ivar}];\n" + index_part = @index ? "#{indent + TAB}#{index_name} = #{ivar};\n" : '' + + set_result = '' + save_result = '' + return_result = '' + if opts[:return] || opts[:assign] + rvar = scope.free_variable + set_result = "var #{rvar} = [];\n#{indent}" + 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}" + 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}" + end end - def chain? - @chain ||= @else_body && @else_body.is_a?(IfNode) + class TryNode < Node + def initialize(try, error, recovery, finally=nil) + @try, @error, @recovery, @finally = try, error, recovery, finally + end + + def line_ending + '' + end + + def statement? + true + end + + def compile(indent, scope, opts={}) + catch_part = @recovery && " catch (#{@error}) {\n#{@recovery.compile(indent + TAB, scope, opts)}\n#{indent}}" + finally_part = @finally && " finally {\n#{@finally.compile(indent + TAB, scope, opts)}\n#{indent}}" + "try {\n#{@try.compile(indent + TAB, scope, opts)}\n#{indent}}#{catch_part}#{finally_part}" + end end - def statement? - @is_statement ||= (@body.statement? || (@else_body && @else_body.statement?)) + class ThrowNode < Node + def initialize(expression) + @expression = expression + end + + def statement? + true + end + + def compile(indent, scope, opts={}) + "throw #{@expression.compile(indent, scope)}" + end end - def line_ending - statement? ? '' : ';' + class ParentheticalNode < Node + def initialize(expressions) + @expressions = expressions + end + + def compile(indent, scope, opts={}) + compiled = @expressions.flatten.compile(indent, scope) + compiled = compiled[0...-1] if compiled[-1..-1] == ';' + opts[:no_paren] ? compiled : "(#{compiled})" + end end - def compile(indent, scope, opts={}) - statement? ? compile_statement(indent, scope, opts) : compile_ternary(indent, scope) + # "if-else" control structure. Look at this node if you want to implement other control + # structures like while, for, loop, etc. + class IfNode < Node + def initialize(condition, body, else_body=nil, tag=nil) + @condition = condition + @body = body && body.flatten + @else_body = else_body && else_body.flatten + @condition = OpNode.new("!", @condition) if tag == :invert + end + + def <<(else_body) + eb = else_body.flatten + @else_body ? @else_body << eb : @else_body = eb + self + end + + # Rewrite a chain of IfNodes with their switch condition for equality. + def rewrite_condition(expression) + @condition = OpNode.new("is", expression, @condition) + @else_body.rewrite_condition(expression) if chain? + self + end + + # Rewrite a chain of IfNodes to add a default case as the final else. + def add_else(expressions) + chain? ? @else_body.add_else(expressions) : @else_body = expressions + self + end + + def chain? + @chain ||= @else_body && @else_body.is_a?(IfNode) + end + + def statement? + @is_statement ||= (@body.statement? || (@else_body && @else_body.statement?)) + end + + def line_ending + statement? ? '' : ';' + end + + def compile(indent, scope, opts={}) + statement? ? compile_statement(indent, scope, opts) : compile_ternary(indent, scope) + end + + def compile_statement(indent, scope, opts) + if_part = "if (#{@condition.compile(indent, scope, :no_paren => true)}) {\n#{Nodes.wrap(@body).compile(indent + TAB, scope, opts)}\n#{indent}}" + else_part = @else_body ? " else {\n#{Nodes.wrap(@else_body).compile(indent + TAB, scope, opts)}\n#{indent}}" : '' + if_part + else_part + end + + def compile_ternary(indent, scope) + if_part = "#{@condition.compile(indent, scope)} ? #{@body.compile(indent, scope)}" + else_part = @else_body ? "#{@else_body.compile(indent, scope)}" : 'null' + "#{if_part} : #{else_part}" + end end - def compile_statement(indent, scope, opts) - if_part = "if (#{@condition.compile(indent, scope, :no_paren => true)}) {\n#{Nodes.wrap(@body).compile(indent + TAB, scope, opts)}\n#{indent}}" - else_part = @else_body ? " else {\n#{Nodes.wrap(@else_body).compile(indent + TAB, scope, opts)}\n#{indent}}" : '' - if_part + else_part - end - - def compile_ternary(indent, scope) - if_part = "#{@condition.compile(indent, scope)} ? #{@body.compile(indent, scope)}" - else_part = @else_body ? "#{@else_body.compile(indent, scope)}" : 'null' - "#{if_part} : #{else_part}" - end -end +end \ No newline at end of file diff --git a/lib/coffee_script/parse_error.rb b/lib/coffee_script/parse_error.rb index 9f49f0bb..88e1e2d2 100644 --- a/lib/coffee_script/parse_error.rb +++ b/lib/coffee_script/parse_error.rb @@ -1,5 +1,8 @@ module CoffeeScript + # Racc will raise this Exception whenever a syntax error occurs. The main + # benefit over the Racc::ParseError is that the CoffeeScript::ParseError is + # line-number aware. class ParseError < Racc::ParseError def initialize(token_id, value, stack) diff --git a/lib/coffee_script/scope.rb b/lib/coffee_script/scope.rb index a09fb7c9..22d3698e 100644 --- a/lib/coffee_script/scope.rb +++ b/lib/coffee_script/scope.rb @@ -1,33 +1,40 @@ -# A class to handle lookups for lexically scoped variables. -class Scope +module CoffeeScript - attr_reader :parent, :temp_variable + # Scope objects form a tree corresponding to the shape of the function + # definitions present in the script. They provide lexical scope, to determine + # whether a variable has been seen before or if it needs to be declared. + class Scope - def initialize(parent=nil) - @parent = parent - @variables = {} - @temp_variable = @parent ? @parent.temp_variable : 'a' - end + attr_reader :parent, :temp_variable - # Look up a variable in lexical scope, or declare it if not found. - def find(name, remote=false) - found = check(name, remote) - return found if found || remote - @variables[name] = true - found - end + # Initialize a scope with its parent, for lookups up the chain. + def initialize(parent=nil) + @parent = parent + @variables = {} + @temp_variable = @parent ? @parent.temp_variable : 'a' + end - # Just check for the pre-definition of a variable. - def check(name, remote=false) - return true if @variables[name] - @parent && @parent.find(name, true) - end + # Look up a variable in lexical scope, or declare it if not found. + def find(name, remote=false) + found = check(name, remote) + return found if found || remote + @variables[name] = true + found + end + + # Just check to see if a variable has already been declared. + def check(name, remote=false) + return true if @variables[name] + @parent && @parent.find(name, true) + end + + # Find an available, short, name for a compiler-generated variable. + def free_variable + @temp_variable.succ! while check(@temp_variable) + @variables[@temp_variable] = true + @temp_variable.dup + end - # Find an available, short variable name. - def free_variable - @temp_variable.succ! while check(@temp_variable) - @variables[@temp_variable] = true - @temp_variable.dup end end \ No newline at end of file diff --git a/lib/coffee_script/value.rb b/lib/coffee_script/value.rb index 19b56ed6..2f0cae3c 100644 --- a/lib/coffee_script/value.rb +++ b/lib/coffee_script/value.rb @@ -1,34 +1,38 @@ -# Instead of producing raw Ruby objects, the Lexer produces values of this -# class, tagged with line number information. -class Value - attr_reader :line +module CoffeeScript - def initialize(value, line) - @value, @line = value, line + # Instead of producing raw Ruby objects, the Lexer produces values of this + # class, wrapping native objects tagged with line number information. + class Value + attr_reader :line + + def initialize(value, line) + @value, @line = value, line + end + + def to_str + @value.to_s + end + alias_method :to_s, :to_str + + def inspect + @value.inspect + end + + def ==(other) + @value == other + end + + def [](index) + @value[index] + end + + def eql?(other) + @value.eql?(other) + end + + def hash + @value.hash + end end - def to_str - @value.to_s - end - alias_method :to_s, :to_str - - def inspect - @value.inspect - end - - def ==(other) - @value == other - end - - def [](index) - @value[index] - end - - def eql?(other) - @value.eql?(other) - end - - def hash - @value.hash - end end \ No newline at end of file