From 16f9a2e6b7bcee351ff44a26105384b67ac1841c Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sun, 21 Mar 2010 23:33:24 -0400 Subject: [PATCH] rewriting the compiler to use half-expression assignment --- src/coffee-script.coffee | 4 ++-- src/command.coffee | 2 +- src/helpers.coffee | 14 +++++------ src/lexer.coffee | 46 ++++++++++++++++++------------------ src/nodes.coffee | 32 ++++++++++++------------- src/rewriter.coffee | 36 ++++++++++++++-------------- test/test_arguments.coffee | 2 +- test/test_compilation.coffee | 4 ++-- test/test_switch.coffee | 2 +- 9 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index f71c4295..bedb9129 100644 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -30,7 +30,7 @@ lexer: new Lexer() # Compile a string of CoffeeScript code to JavaScript, using the Coffee/Jison # compiler. exports.compile: compile: (code, options) -> - options ||= {} + options: or {} try (parser.parse lexer.tokenize code).compile options catch err @@ -68,7 +68,7 @@ exports.extend: (func) -> parser.lexer: { lex: -> token: @tokens[@pos] or [""] - @pos += 1 + @pos: + 1 this.yylineno: token[2] this.yytext: token[1] token[0] diff --git a/src/command.coffee b/src/command.coffee index 8eb26bfd..739784f8 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -94,7 +94,7 @@ compile_stdio: -> code: '' process.stdio.open() process.stdio.addListener 'data', (string) -> - code += string if string + code: + string if string process.stdio.addListener 'close', -> compile_script 'stdio', code diff --git a/src/helpers.coffee b/src/helpers.coffee index 2e83e574..0f34a2d0 100644 --- a/src/helpers.coffee +++ b/src/helpers.coffee @@ -22,7 +22,7 @@ helpers.count: count: (string, letter) -> num: 0 pos: string.indexOf(letter) while pos isnt -1 - num += 1 + num: + 1 pos: string.indexOf(letter, pos + 1) num @@ -60,27 +60,27 @@ helpers.del: del: (obj, key) -> # contents of the string. This method allows us to have strings within # interpolations within strings, ad infinitum. helpers.balanced_string: balanced_string: (str, delimited, options) -> - options ||= {} + options: or {} slash: delimited[0][0] is '/' levels: [] i: 0 while i < str.length if levels.length and starts str, '\\', i - i += 1 + i: + 1 else for pair in delimited [open, close]: pair if levels.length and starts(str, close, i) and levels[levels.length - 1] is pair levels.pop() - i += close.length - 1 - i += 1 unless levels.length + i: + close.length - 1 + i: + 1 unless levels.length break else if starts str, open, i levels.push(pair) - i += open.length - 1 + i: + open.length - 1 break break if not levels.length or slash and starts str, '\n', i - i += 1 + i: + 1 if levels.length return false if slash throw new Error "SyntaxError: Unterminated ${levels.pop()[0]} starting on line ${@line + 1}" diff --git a/src/lexer.coffee b/src/lexer.coffee index e650551e..c48d0631 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -99,7 +99,7 @@ exports.Lexer: class Lexer tag: id.toUpperCase() if not accessed and include(KEYWORDS, id) @identifier_error id if include RESERVED, id tag: 'LEADING_WHEN' if tag is 'WHEN' and include LINE_BREAK, @tag() - @i += id.length + @i: + id.length if not accessed tag: id: CONVERSIONS[id] if include COFFEE_ALIASES, id return @tag_half_assignment(tag) if @prev() and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag @@ -110,7 +110,7 @@ exports.Lexer: class Lexer number_token: -> return false unless number: @match NUMBER, 1 @token 'NUMBER', number - @i += number.length + @i: + number.length true # Matches strings, including multi-line strings. Ensures that quotation marks @@ -121,8 +121,8 @@ exports.Lexer: class Lexer @balanced_token(['"', '"'], ['${', '}']) or @balanced_token ["'", "'"] @interpolate_string string.replace(STRING_NEWLINES, " \\\n") - @line += count string, "\n" - @i += string.length + @line: + count string, "\n" + @i: + string.length true # Matches heredocs, adjusting indentation to the correct level, as heredocs @@ -132,8 +132,8 @@ exports.Lexer: class Lexer quote: match[1].substr(0, 1) doc: @sanitize_heredoc match[2] or match[4], quote @interpolate_string "$quote$doc$quote" - @line += count match[1], "\n" - @i += match[1].length + @line: + count match[1], "\n" + @i: + match[1].length true # Matches JavaScript interpolated directly into the source via backticks. @@ -141,7 +141,7 @@ exports.Lexer: class Lexer return false unless starts @chunk, '`' return false unless script: @balanced_token ['`', '`'] @token 'JS', script.replace(JS_CLEANER, '') - @i += script.length + @i: + script.length true # Matches regular expression literals. Lexing regular expressions is difficult @@ -152,7 +152,7 @@ exports.Lexer: class Lexer return false unless @chunk.match REGEX_START return false if include NOT_REGEX, @tag() return false unless regex: @balanced_token ['/', '/'] - regex += (flags: @chunk.substr(regex.length).match(REGEX_FLAGS)) + regex: + (flags: @chunk.substr(regex.length).match(REGEX_FLAGS)) if regex.match REGEX_INTERPOLATION str: regex.substring(1).split('/')[0] str: str.replace REGEX_ESCAPE, (escaped) -> '\\' + escaped @@ -161,7 +161,7 @@ exports.Lexer: class Lexer @tokens: @tokens.concat [[',', ','], ['STRING', "'$flags'"], [')', ')'], [')', ')']] else @token 'REGEX', regex - @i += regex.length + @i: + regex.length true # Matches a token in which which the passed delimiter pairs must be correctly @@ -173,13 +173,13 @@ exports.Lexer: class Lexer # so they're treated as real tokens, like any other part of the language. comment_token: -> return false unless comment: @match COMMENT, 1 - @line += (comment.match(MULTILINER) or []).length + @line: + (comment.match(MULTILINER) or []).length lines: compact comment.replace(COMMENT_CLEANER, '').split(MULTILINER) i: @tokens.length - 1 if @unfinished() - i -= 1 while @tokens[i] and not include LINE_BREAK, @tokens[i][0] + i: - 1 while @tokens[i] and not include LINE_BREAK, @tokens[i][0] @tokens.splice(i + 1, 0, ['COMMENT', lines, @line], ['TERMINATOR', '\n', @line]) - @i += comment.length + @i: + comment.length true # Matches newlines, indents, and outdents, and determines which is which. @@ -194,8 +194,8 @@ exports.Lexer: class Lexer # can close multiple indents, so we need to know how far in we happen to be. line_token: -> return false unless indent: @match MULTI_DENT, 1 - @line += indent.match(MULTILINER).length - @i += indent.length + @line: + indent.match(MULTILINER).length + @i : + indent.length prev: @prev(2) size: indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length next_character: @chunk.match(MULTI_DENT)[4] @@ -219,7 +219,7 @@ exports.Lexer: class Lexer while move_out > 0 and @indents.length last_indent: @indents.pop() @token 'OUTDENT', last_indent - move_out -= last_indent + move_out: - last_indent @token 'TERMINATOR', "\n" unless @tag() is 'TERMINATOR' or no_newlines true @@ -229,7 +229,7 @@ exports.Lexer: class Lexer return false unless space: @match WHITESPACE, 1 prev: @prev() prev.spaced: true if prev - @i += space.length + @i: + space.length true # Generate a newline token. Consecutive newlines get merged together. @@ -253,7 +253,7 @@ exports.Lexer: class Lexer value: match and match[1] space: match and match[2] @tag_parameters() if value and value.match(CODE) - value ||= @chunk.substr(0, 1) + value: or @chunk.substr(0, 1) prev_spaced: @prev() and @prev().spaced tag: value if value.match(ASSIGNMENT) @@ -271,7 +271,7 @@ exports.Lexer: class Lexer else if include(CALLABLE, @tag()) and not prev_spaced tag: 'CALL_START' if value is '(' tag: 'INDEX_START' if value is '[' - @i += value.length + @i: + value.length return @tag_half_assignment(tag) if space and prev_spaced and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag @token tag, value true @@ -311,7 +311,7 @@ exports.Lexer: class Lexer return if @tag() isnt ')' i: 0 while true - i += 1 + i: + 1 tok: @prev(i) return if not tok switch tok[0] @@ -354,13 +354,13 @@ exports.Lexer: class Lexer [i, pi]: [1, 1] while i < str.length - 1 if starts str, '\\', i - i += 1 + i: + 1 else if match: str.substring(i).match INTERPOLATION [group, interp]: match interp: "this.${ interp.substring(1) }" if starts interp, '@' tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i tokens.push ['IDENTIFIER', interp] - i += group.length - 1 + i: + group.length - 1 pi: i + 1 else if (expr: balanced_string str.substring(i), [['${', '}']]) tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i @@ -371,9 +371,9 @@ exports.Lexer: class Lexer tokens.push ['TOKENS', nested] else tokens.push ['STRING', "$quote$quote"] - i += expr.length - 1 + i: + expr.length - 1 pi: i + 1 - i += 1 + i: + 1 tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i and pi < str.length - 1 tokens.unshift ['STRING', "''"] unless tokens[0][0] is 'STRING' for token, i in tokens diff --git a/src/nodes.coffee b/src/nodes.coffee index 9f5d34a6..a3cdd484 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -79,7 +79,7 @@ exports.BaseNode: class BaseNode idt: (tabs) -> idt: @tab or '' num: (tabs or 0) + 1 - idt += TAB while num -= 1 + idt: + TAB while num: - 1 idt # Construct a node that returns the current node's result. @@ -112,7 +112,7 @@ exports.BaseNode: class BaseNode # `toString` representation of the node, for inspecting the parse tree. # This is what `coffee --nodes` prints out. toString: (idt) -> - idt ||= '' + idt: or '' '\n' + idt + @type + (child.toString(idt + TAB) for child in @children).join('') # Default implementations of the common node identification methods. Nodes @@ -162,14 +162,14 @@ exports.Expressions: class Expressions extends BaseNode make_return: -> idx: @expressions.length - 1 last: @expressions[idx] - last: @expressions[idx -= 1] if last instanceof CommentNode + last: @expressions[idx: - 1] if last instanceof CommentNode return this if not last or last instanceof ReturnNode @expressions[idx]: last.make_return() unless last.contains_pure_statement() this # An **Expressions** is the only node that can serve as the root. compile: (o) -> - o ||= {} + o: or {} if o.scope then super(o) else @compile_root(o) compile_node: (o) -> @@ -315,11 +315,11 @@ exports.ValueNode: class ValueNode extends BaseNode temp: o.scope.free_variable() complete: "($temp = $complete)$@SOAK" + (baseline: temp + prop.compile(o)) else - complete: complete + @SOAK + (baseline += prop.compile(o)) + complete: complete + @SOAK + (baseline: + prop.compile(o)) else part: prop.compile(o) - baseline += part - complete += part + baseline: + part + complete: + part @last: part if op and soaked then "($complete)" else complete @@ -772,7 +772,7 @@ exports.CodeNode: class CodeNode extends BaseNode splat.trailings.push(param) else params.push(param) - i += 1 + i: + 1 params: (param.compile(o) for param in params) @body.make_return() (o.scope.parameter(param)) for param in params @@ -798,7 +798,7 @@ exports.CodeNode: class CodeNode extends BaseNode child.traverse block for child in @real_children() toString: (idt) -> - idt ||= '' + idt: or '' children: (child.toString(idt + TAB) for child in @real_children()).join('') "\n$idt$children" @@ -824,7 +824,7 @@ exports.SplatNode: class SplatNode extends BaseNode i: 0 for trailing in @trailings o.scope.assign(trailing.compile(o), "arguments[arguments.length - $@trailings.length + $i]") - i += 1 + i: + 1 "$name = Array.prototype.slice.call(arguments, $@index, arguments.length - ${@trailings.length})" # A compiling a splat as a destructuring assignment means slicing arguments @@ -850,7 +850,7 @@ SplatNode.compile_mixed_array: (list, o) -> else code: "[$code]" args.push(if i is 0 then code else ".concat($code)") - i += 1 + i: + 1 args.join('') #### WhileNode @@ -924,7 +924,7 @@ exports.OpNode: class OpNode extends BaseNode PREFIX_OPERATORS: ['typeof', 'delete'] constructor: (operator, first, second, flip) -> - @type += ' ' + operator + @type: + ' ' + operator @children: compact [@first: first, @second: second] @operator: @CONVERSIONS[operator] or operator @flip: !!flip @@ -1221,12 +1221,12 @@ exports.IfNode: class IfNode extends BaseNode # If the `else_body` is an **IfNode** itself, then we've got an *if-else* chain. is_chain: -> - @chain ||= @else_body and @else_body instanceof IfNode + @chain: or @else_body and @else_body instanceof IfNode # The **IfNode** only compiles into a statement if either of its bodies needs # to be a statement. Otherwise a ternary is safe. is_statement: -> - @statement ||= !!(@comment or @tags.statement or @body.is_statement() or (@else_body and @else_body.is_statement())) + @statement: or !!(@comment or @tags.statement or @body.is_statement() or (@else_body and @else_body.is_statement())) compile_condition: (o) -> (cond.compile(o) for cond in flatten([@condition])).join(' || ') @@ -1235,8 +1235,8 @@ exports.IfNode: class IfNode extends BaseNode if @is_statement() then @compile_statement(o) else @compile_ternary(o) make_return: -> - @body &&= @body.make_return() - @else_body &&= @else_body.make_return() + @body: and @body.make_return() + @else_body: and @else_body.make_return() this # Compile the **IfNode** as a regular *if-else* statement. Flattened chains diff --git a/src/rewriter.coffee b/src/rewriter.coffee index 8fde969b..ecb6ca15 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -46,7 +46,7 @@ exports.Rewriter: class Rewriter while true break unless @tokens[i] move: block(@tokens[i - 1], @tokens[i], @tokens[i + 1], i) - i += move + i: + move true # Massage newlines and indentations so that comments don't have to be @@ -88,20 +88,20 @@ exports.Rewriter: class Rewriter switch token[0] when 'CALL_START' then parens.push(0) when 'INDEX_START' then brackets.push(0) - when '(' then parens[parens.length - 1] += 1 - when '[' then brackets[brackets.length - 1] += 1 + when '(' then parens[parens.length - 1]: + 1 + when '[' then brackets[brackets.length - 1]: + 1 when ')' if parens[parens.length - 1] is 0 parens.pop() token[0]: 'CALL_END' else - parens[parens.length - 1] -= 1 + parens[parens.length - 1]: - 1 when ']' if brackets[brackets.length - 1] == 0 brackets.pop() token[0]: 'INDEX_END' else - brackets[brackets.length - 1] -= 1 + brackets[brackets.length - 1]: - 1 return 1 # Methods may be optionally called without parentheses, for simple cases. @@ -113,15 +113,15 @@ exports.Rewriter: class Rewriter @scan_tokens (prev, token, post, i) => tag: token[0] switch tag - when 'CALL_START' then calls += 1 - when 'CALL_END' then calls -= 1 + when 'CALL_START' then calls: + 1 + when 'CALL_END' then calls: - 1 when 'INDENT' then stack.push(0) when 'OUTDENT' last: stack.pop() - stack[stack.length - 1] += last + stack[stack.length - 1]: + last open: stack[stack.length - 1] > 0 if tag is 'CALL_END' and calls < 0 and open - stack[stack.length - 1] -= 1 + stack[stack.length - 1]: - 1 @tokens.splice(i, 0, ['CALL_END', ')', token[2]]) return 2 if !post? or include IMPLICIT_END, tag @@ -137,7 +137,7 @@ exports.Rewriter: class Rewriter return 1 unless prev and include(IMPLICIT_FUNC, prev[0]) and include IMPLICIT_CALL, tag calls: 0 @tokens.splice(i, 0, ['CALL_START', '(', token[2]]) - stack[stack.length - 1] += 1 + stack[stack.length - 1]: + 1 return 2 # Because our grammar is LALR(1), it can't handle some single-line @@ -154,7 +154,7 @@ exports.Rewriter: class Rewriter idx: i + 1 parens: 0 while true - idx += 1 + idx: + 1 tok: @tokens[idx] pre: @tokens[idx - 1] if (not tok or @@ -164,8 +164,8 @@ exports.Rewriter: class Rewriter insertion: if pre[0] is "," then idx - 1 else idx @tokens.splice(insertion, 0, ['OUTDENT', 2, token[2]]) break - parens += 1 if tok[0] is '(' - parens -= 1 if tok[0] is ')' + parens: + 1 if tok[0] is '(' + parens: - 1 if tok[0] is ')' return 1 unless token[0] is 'THEN' @tokens.splice(i, 1) return 0 @@ -178,11 +178,11 @@ exports.Rewriter: class Rewriter @scan_tokens (prev, token, post, i) => for pair in pairs [open, close]: pair - levels[open] ||= 0 + levels[open]: or 0 if token[0] is open open_line[open]: token[2] if levels[open] == 0 - levels[open] += 1 - levels[open] -= 1 if token[0] is close + levels[open]: + 1 + levels[open]: - 1 if token[0] is close throw new Error("too many ${token[1]} on line ${token[2] + 1}") if levels[open] < 0 return 1 unclosed: key for key, value of levels when value > 0 @@ -218,14 +218,14 @@ exports.Rewriter: class Rewriter return 1 else if include EXPRESSION_END, tag if debt[inv] > 0 - debt[inv] -= 1 + debt[inv]: - 1 @tokens.splice i, 1 return 0 else match: stack.pop() mtag: match[0] return 1 if tag is INVERSES[mtag] - debt[mtag] += 1 + debt[mtag]: + 1 val: if mtag is 'INDENT' then match[1] else INVERSES[mtag] @tokens.splice i, 0, [INVERSES[mtag], val] return 1 diff --git a/test/test_arguments.coffee b/test/test_arguments.coffee index fc102056..66b6c8ac 100644 --- a/test/test_arguments.coffee +++ b/test/test_arguments.coffee @@ -19,7 +19,7 @@ ok(area( sum_of_args: -> sum: 0 - sum += val for val in arguments + sum: + val for val in arguments sum ok sum_of_args(1, 2, 3, 4, 5) is 15 \ No newline at end of file diff --git a/test/test_compilation.coffee b/test/test_compilation.coffee index 1dd40110..1f40a734 100644 --- a/test/test_compilation.coffee +++ b/test/test_compilation.coffee @@ -22,7 +22,7 @@ class SplitNode extends BaseNode # and creates a SplitNode. CoffeeScript.extend -> return false unless variable: @match(/^--(\w+)--/, 1) - @i += variable.length + 4 + @i: + variable.length + 4 @token 'EXTENSION', new SplitNode(variable) true @@ -46,7 +46,7 @@ class WordArrayNode extends BaseNode CoffeeScript.extend -> return false unless words: @chunk.match(/^%w\{(.*?)\}/) - @i += words[0].length + @i: + words[0].length @token 'EXTENSION', new WordArrayNode(words[1].split(/\s+/)) true diff --git a/test/test_switch.coffee b/test/test_switch.coffee index 97475cbc..fe3d7bb3 100644 --- a/test/test_switch.coffee +++ b/test/test_switch.coffee @@ -33,7 +33,7 @@ ok !func(8) # Should cache the switch value, if anything fancier than a literal. num: 5 -result: switch num += 5 +result: switch num: + 5 when 5 then false when 15 then false when 10 then true