lib/coffee_script/rewriter.rb in coffee-script-0.2.6 vs lib/coffee_script/rewriter.rb in coffee-script-0.3.0

- old
+ new

@@ -4,42 +4,53 @@ # emits is rewritten by the Rewriter, smoothing out ambiguities, mis-nested # indentation, and single-line flavors of expressions. class Rewriter # Tokens that must be balanced. - BALANCED_PAIRS = [['(', ')'], ['[', ']'], ['{', '}'], [:INDENT, :OUTDENT]] + BALANCED_PAIRS = [['(', ')'], ['[', ']'], ['{', '}'], [:INDENT, :OUTDENT], + [:PARAM_START, :PARAM_END], [:CALL_START, :CALL_END], [:INDEX_START, :INDEX_END]] # Tokens that signal the start of a balanced pair. EXPRESSION_START = BALANCED_PAIRS.map {|pair| pair.first } # Tokens that signal the end of a balanced pair. EXPRESSION_TAIL = BALANCED_PAIRS.map {|pair| pair.last } # Tokens that indicate the close of a clause of an expression. EXPRESSION_CLOSE = [:CATCH, :WHEN, :ELSE, :FINALLY] + EXPRESSION_TAIL + # Tokens pairs that, in immediate succession, indicate an implicit call. + IMPLICIT_FUNC = [:IDENTIFIER, :SUPER, ')', :CALL_END, ']', :INDEX_END] + IMPLICIT_END = [:IF, :UNLESS, :FOR, :WHILE, "\n", :OUTDENT] + IMPLICIT_CALL = [:IDENTIFIER, :NUMBER, :STRING, :JS, :REGEX, :NEW, :PARAM_START, + :TRY, :DELETE, :INSTANCEOF, :TYPEOF, :SWITCH, :ARGUMENTS, + :TRUE, :FALSE, :YES, :NO, :ON, :OFF, '!', '!!', :NOT, + '->', '=>', '[', '(', '{'] + # The inverse mappings of token pairs we're trying to fix up. INVERSES = BALANCED_PAIRS.inject({}) do |memo, pair| memo[pair.first] = pair.last memo[pair.last] = pair.first memo end # 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_CLOSERS = ["\n", :CATCH, :FINALLY, :ELSE, :OUTDENT, :LEADING_WHEN] + SINGLE_LINERS = [:ELSE, "->", "=>", :TRY, :FINALLY, :THEN] + SINGLE_CLOSERS = ["\n", :CATCH, :FINALLY, :ELSE, :OUTDENT, :LEADING_WHEN, :PARAM_START] # Rewrite the token stream in multiple passes, one logical filter at # a time. This could certainly be changed into a single pass through the # stream, with a big ol' efficient switch, but it's much nicer like this. def rewrite(tokens) @tokens = tokens adjust_comments remove_leading_newlines remove_mid_expression_newlines move_commas_outside_outdents + close_open_calls_and_indexes + add_implicit_parentheses add_implicit_indentation ensure_balance(*BALANCED_PAIRS) rewrite_closing_parens @tokens end @@ -68,11 +79,11 @@ (before[0] == :OUTDENT && after[0] == :INDENT)) && before[1] == after[1] @tokens.delete_at(i + 2) @tokens.delete_at(i - 2) next 0 - elsif prev[0] == "\n" && [:INDENT, :OUTDENT].include?(after[0]) + elsif prev[0] == "\n" && [:INDENT].include?(after[0]) @tokens.delete_at(i + 2) @tokens[i - 1] = after next 1 elsif !["\n", :INDENT, :OUTDENT].include?(prev[0]) @tokens.insert(i, ["\n", Value.new("\n", token[1].line)]) @@ -109,28 +120,60 @@ end next 1 end end + # We've tagged the opening parenthesis of a method call, and the opening + # bracket of an indexing operation. Match them with their close. + def close_open_calls_and_indexes + parens, brackets = [0], [0] + scan_tokens do |prev, token, post, i| + case token[0] + when :CALL_START then parens.push(0) + when :INDEX_START then brackets.push(0) + when '(' then parens[-1] += 1 + when '[' then brackets[-1] += 1 + when ')' + if parens.last == 0 + parens.pop + token[0] = :CALL_END + else + parens[-1] -= 1 + end + when ']' + if brackets.last == 0 + brackets.pop + token[0] = :INDEX_END + else + brackets[-1] -= 1 + end + end + next 1 + end + end + # Because our grammar is LALR(1), it can't handle some single-line # expressions that lack ending delimiters. Use the lexer to add the implicit # blocks, so it doesn't need to. # ')' can close a single-line block, but we need to make sure it's balanced. def add_implicit_indentation scan_tokens do |prev, token, post, i| next 1 unless SINGLE_LINERS.include?(token[0]) && post[0] != :INDENT && !(token[0] == :ELSE && post[0] == :IF) # Elsifs shouldn't get blocks. + starter = token[0] line = token[1].line @tokens.insert(i + 1, [:INDENT, Value.new(2, line)]) idx = i + 1 parens = 0 loop do idx += 1 tok = @tokens[idx] - if !tok || SINGLE_CLOSERS.include?(tok[0]) || - (tok[0] == ')' && parens == 0) - @tokens.insert(idx, [:OUTDENT, Value.new(2, line)]) + if (!tok || SINGLE_CLOSERS.include?(tok[0]) || + (tok[0] == ')' && parens == 0)) && + !(starter == :ELSE && tok[0] == :ELSE) + insertion = @tokens[idx - 1][0] == "," ? idx - 1 : idx + @tokens.insert(insertion, [:OUTDENT, Value.new(2, line)]) break end parens += 1 if tok[0] == '(' parens -= 1 if tok[0] == ')' end @@ -138,28 +181,55 @@ @tokens.delete_at(i) next 0 end end + # Methods may be optionally called without parentheses, for simple cases. + # Insert the implicit parentheses here, so that the parser doesn't have to + # deal with them. + def add_implicit_parentheses + stack = [0] + scan_tokens do |prev, token, post, i| + stack.push(0) if token[0] == :INDENT + if token[0] == :OUTDENT + last = stack.pop + stack[-1] += last + end + if stack.last > 0 && (IMPLICIT_END.include?(token[0]) || post.nil?) + idx = token[0] == :OUTDENT ? i + 1 : i + stack.last.times { @tokens.insert(idx, [:CALL_END, Value.new(')', token[1].line)]) } + size, stack[-1] = stack[-1] + 1, 0 + next size + end + next 1 unless IMPLICIT_FUNC.include?(prev[0]) && IMPLICIT_CALL.include?(token[0]) + @tokens.insert(i, [:CALL_START, Value.new('(', token[1].line)]) + stack[-1] += 1 + next 2 + end + end + # Ensure that all listed pairs of tokens are correctly balanced throughout # the course of the token stream. def ensure_balance(*pairs) - levels = Hash.new(0) + puts "\nbefore ensure_balance: #{@tokens.inspect}" if ENV['VERBOSE'] + levels, lines = Hash.new(0), Hash.new scan_tokens do |prev, token, post, i| pairs.each do |pair| open, close = *pair levels[open] += 1 if token[0] == open levels[open] -= 1 if token[0] == close + lines[token[0]] = token[1].line raise ParseError.new(token[0], token[1], nil) if levels[open] < 0 end next 1 end unclosed = levels.detect {|k, v| v > 0 } - raise SyntaxError, "unclosed '#{unclosed[0]}'" if unclosed + sym = unclosed && unclosed[0] + raise ParseError.new(sym, Value.new(sym, lines[sym]), nil, "unclosed '#{sym}'") if unclosed end # We'd like to support syntax like this: - # el.click(event => + # el.click((event) -> # el.hide()) # In order to accomplish this, move outdents that follow closing parens # inwards, safely. The steps to accomplish this are: # # 1. Check that all paired tokens are balanced and in order. \ No newline at end of file