rewriting the compiler to use half-expression assignment

This commit is contained in:
Jeremy Ashkenas
2010-03-21 23:33:24 -04:00
parent cbfe7f5822
commit 16f9a2e6b7
9 changed files with 71 additions and 71 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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