From f9dff6ffc48d32dee6ff1f7748ce028d1ff88252 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sat, 24 Jul 2010 22:36:50 -0700 Subject: [PATCH] added and= and or= to the language. --- lib/lexer.js | 34 ++++++++++++---------------------- src/coffee-script.coffee | 2 +- src/command.coffee | 2 +- src/lexer.coffee | 26 +++++++++----------------- src/nodes.coffee | 24 ++++++++++++------------ src/rewriter.coffee | 2 +- 6 files changed, 36 insertions(+), 54 deletions(-) diff --git a/lib/lexer.js b/lib/lexer.js index 5423badf..14f853e9 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -1,5 +1,5 @@ (function(){ - var ASSIGNED, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_KEYWORDS, COMMENT, CONVERSIONS, HALF_ASSIGNMENTS, HEREDOC, HEREDOC_INDENT, IDENTIFIER, INTERPOLATION, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, LAST_DENT, LAST_DENTS, LINE_BREAK, Lexer, MULTILINER, MULTI_DENT, NEXT_CHARACTER, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX_END, REGEX_ESCAPE, REGEX_INTERPOLATION, REGEX_START, RESERVED, Rewriter, STRING_NEWLINES, WHITESPACE, _a, _b, _c, compact, count, helpers, include, starts; + var ASSIGNED, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_KEYWORDS, COMMENT, CONVERSIONS, HEREDOC, HEREDOC_INDENT, IDENTIFIER, INTERPOLATION, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, LAST_DENT, LAST_DENTS, LINE_BREAK, Lexer, MULTILINER, MULTI_DENT, NEXT_CHARACTER, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX_END, REGEX_ESCAPE, REGEX_INTERPOLATION, REGEX_START, RESERVED, Rewriter, STRING_NEWLINES, WHITESPACE, _a, _b, _c, compact, count, helpers, include, starts; var __slice = Array.prototype.slice; if (typeof process !== "undefined" && process !== null) { _a = require('./rewriter'); @@ -105,9 +105,6 @@ if (include(COFFEE_ALIASES, id)) { tag = (id = CONVERSIONS[id]); } - if (this.prev() && this.prev()[0] === 'ASSIGN' && include(HALF_ASSIGNMENTS, tag)) { - return this.tagHalfAssignment(tag); - } } this.token(tag, id); if (close_index) { @@ -299,7 +296,7 @@ return true; }; Lexer.prototype.literalToken = function() { - var match, prevSpaced, space, tag, value; + var _d, match, prevSpaced, space, tag, value; match = this.chunk.match(OPERATOR); value = match && match[1]; space = match && match[2]; @@ -307,10 +304,16 @@ this.tagParameters(); } value = value || this.chunk.substr(0, 1); + this.i += value.length; prevSpaced = this.prev() && this.prev().spaced; tag = value; - if (value === '=' && include(JS_FORBIDDEN, this.value)) { - this.assignmentError(); + if (value === '=') { + if (include(JS_FORBIDDEN, this.value())) { + this.assignmentError(); + } + if (('or' === (_d = this.value()) || 'and' === _d)) { + return this.tag(1, CONVERSIONS[this.value()] + '='); + } } if (value === ';') { tag = 'TERMINATOR'; @@ -329,10 +332,6 @@ } } } - this.i += value.length; - if (space && prevSpaced && this.prev()[0] === 'ASSIGN' && include(HALF_ASSIGNMENTS, tag)) { - return this.tagHalfAssignment(tag); - } this.token(tag, value); return true; }; @@ -371,15 +370,6 @@ } return doc.replace(MULTILINER, "\\n").replace(new RegExp(options.quote, 'g'), ("\\" + options.quote)); }; - Lexer.prototype.tagHalfAssignment = function(tag) { - var last; - if (tag === 'OP?') { - tag = '?'; - } - last = this.tokens.pop(); - this.tokens.push([("" + tag + "="), ("" + tag + "="), last[2]]); - return true; - }; Lexer.prototype.tagParameters = function() { var _d, i, tok; if (this.tag() !== ')') { @@ -616,12 +606,12 @@ NOT_REGEX = ['NUMBER', 'REGEX', '++', '--', 'FALSE', 'NULL', 'TRUE', ']']; CALLABLE = ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING', '@', 'THIS', '?', '::']; LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR']; - HALF_ASSIGNMENTS = ['-', '+', '/', '*', '%', '||', '&&', '?', 'OP?']; CONVERSIONS = { 'and': '&&', 'or': '||', 'is': '==', 'isnt': '!=', - 'not': '!' + 'not': '!', + '===': '==' }; })(); diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index ac578a54..7970e9e2 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 diff --git a/src/command.coffee b/src/command.coffee index 9076e27e..b89e588c 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -156,7 +156,7 @@ printTokens = (tokens) -> parseOptions = -> optionParser = new optparse.OptionParser SWITCHES, BANNER o = options = optionParser.parse(process.argv[2...process.argv.length]) - options.compile ||= !!o.output + options.compile or= !!o.output options.run = not (o.compile or o.print or o.lint) options.print = !! (o.print or (o.eval or o.stdio and o.compile)) sources = options.arguments diff --git a/src/lexer.coffee b/src/lexer.coffee index 548a5233..e21c4a85 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -100,7 +100,6 @@ exports.Lexer = class Lexer @identifierError id unless forcedIdentifier tag = id = CONVERSIONS[id] if include COFFEE_ALIASES, id - return @tagHalfAssignment tag if @prev() and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag @token tag, id @token ']', ']' if close_index true @@ -258,10 +257,14 @@ exports.Lexer = class Lexer value = match and match[1] space = match and match[2] @tagParameters() if value and value.match CODE - value ||= @chunk.substr 0, 1 + value or= @chunk.substr 0, 1 + @i += value.length prevSpaced = @prev() and @prev().spaced tag = value - @assignmentError() if value is '=' and include JS_FORBIDDEN, @value + if value is '=' + @assignmentError() if include JS_FORBIDDEN, @value() + if @value() in ['or', 'and'] + return @tag 1, CONVERSIONS[@value()] + '=' if value is ';' tag = 'TERMINATOR' else if value is '?' and prevSpaced @@ -273,8 +276,6 @@ exports.Lexer = class Lexer tag = 'INDEX_START' @tag 1, 'INDEX_SOAK' if @tag() is '?' @tag 1, 'INDEX_PROTO' if @tag() is '::' - @i += value.length - return @tagHalfAssignment tag if space and prevSpaced and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag @token tag, value true @@ -309,13 +310,6 @@ exports.Lexer = class Lexer doc.replace(MULTILINER, "\\n") .replace(new RegExp(options.quote, 'g'), "\\$options.quote") - # Tag a half assignment. - tagHalfAssignment: (tag) -> - tag = '?' if tag is 'OP?' - last = @tokens.pop() - @tokens.push ["$tag=", "$tag=", last[2]] - true - # A source of ambiguity in our grammar used to be parameter lists in function # definitions versus argument lists in function calls. Walk backwards, tagging # parameters specially in order to make things easier for the parser. @@ -351,7 +345,7 @@ exports.Lexer = class Lexer # contents of the string. This method allows us to have strings within # interpolations within strings, ad infinitum. balancedString: (str, delimited, options) -> - options ||= {} + options or= {} slash = delimited[0][0] is '/' levels = [] i = 0 @@ -388,7 +382,7 @@ exports.Lexer = class Lexer # new Lexer, tokenize the interpolated contents, and merge them into the # token stream. interpolateString: (str, options) -> - options ||= {} + options or= {} if str.length < 3 or not starts str, '"' @token 'STRING', str else @@ -556,9 +550,6 @@ CALLABLE = ['IDENTIFIER', 'SUPER', ')', ']', '}', 'STRING', '@', 'THIS', '?', ': # avoid an ambiguity in the grammar. LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR'] -# Half-assignments... -HALF_ASSIGNMENTS = ['-', '+', '/', '*', '%', '||', '&&', '?', 'OP?'] - # Conversions from CoffeeScript operators into JavaScript ones. CONVERSIONS = { 'and': '&&' @@ -566,4 +557,5 @@ CONVERSIONS = { 'is': '==' 'isnt': '!=' 'not': '!' + '===': '==' } diff --git a/src/nodes.coffee b/src/nodes.coffee index d28d12cd..45136371 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -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, override) -> - idt ||= '' + idt or= '' children = (child.toString idt + TAB for child in @collectChildren()).join('') '\n' + idt + (override or @class) + children @@ -193,7 +193,7 @@ exports.Expressions = class Expressions extends BaseNode # An **Expressions** is the only node that can serve as the root. compile: (o) -> - o ||= {} + o or= {} if o.scope then super(o) else @compileRoot(o) compileNode: (o) -> @@ -354,7 +354,7 @@ exports.ValueNode = class ValueNode extends BaseNode only = del o, 'onlyFirst' op = del o, 'operation' props = if only then @properties[0...@properties.length - 1] else @properties - o.chainRoot ||= this + o.chainRoot or= this baseline = @base.compile o baseline = "($baseline)" if @hasProperties() and (@base instanceof ObjectNode or @isNumber()) complete = @last = baseline @@ -502,7 +502,7 @@ exports.AccessorNode = class AccessorNode extends BaseNode compileNode: (o) -> name = @name.compile o - o.chainRoot.wrapped ||= @soakNode + o.chainRoot.wrapped or= @soakNode namePart = if name.match(IS_STRING) then "[$name]" else ".$name" @prototype + namePart @@ -518,7 +518,7 @@ exports.IndexNode = class IndexNode extends BaseNode @index = index compileNode: (o) -> - o.chainRoot.wrapped ||= @soakNode + o.chainRoot.wrapped or= @soakNode idx = @index.compile o prefix = if @proto then '.prototype' else '' "$prefix[$idx]" @@ -570,7 +570,7 @@ exports.RangeNode = class RangeNode extends BaseNode [from, to] = [parseInt(@fromNum, 10), parseInt(@toNum, 10)] idx = del o, 'index' step = del o, 'step' - step &&= "$idx += ${step.compile(o)}" + step and= "$idx += ${step.compile(o)}" if from <= to "$idx = $from; $idx <$@equals $to; ${step or "$idx++"}" else @@ -721,8 +721,8 @@ exports.ClassNode = class ClassNode extends BaseNode continue if func instanceof CodeNode and func.bound func.bound = false - constScope ||= new Scope(o.scope, constructor.body, constructor) - me ||= constScope.freeVariable() + constScope or= new Scope(o.scope, constructor.body, constructor) + me or= constScope.freeVariable() pname = pvar.compile(o) constructor.body.push new ReturnNode literal 'this' if constructor.body.empty() constructor.body.unshift literal "this.${pname} = function(){ return ${className}.prototype.${pname}.apply($me, arguments); }" @@ -902,7 +902,7 @@ exports.CodeNode = class CodeNode extends BaseNode traverseChildren: (crossScope, func) -> super(crossScope, func) if crossScope toString: (idt) -> - idt ||= '' + idt or= '' children = (child.toString(idt + TAB) for child in @collectChildren()).join('') "\n$idt$children" @@ -1385,7 +1385,7 @@ exports.IfNode = class IfNode extends BaseNode # The **IfNode** only compiles into a statement if either of its bodies needs # to be a statement. Otherwise a ternary is safe. isStatement: -> - @statement ||= !!(@tags.statement or @bodyNode().isStatement() or (@elseBody and @elseBodyNode().isStatement())) + @statement or= !!(@tags.statement or @bodyNode().isStatement() or (@elseBody and @elseBodyNode().isStatement())) compileCondition: (o) -> (cond.compile(o) for cond in flatten([@condition])).join(' || ') @@ -1395,8 +1395,8 @@ exports.IfNode = class IfNode extends BaseNode makeReturn: -> if @isStatement() - @body &&= @ensureExpressions(@body.makeReturn()) - @elseBody &&= @ensureExpressions(@elseBody.makeReturn()) + @body and= @ensureExpressions(@body.makeReturn()) + @elseBody and= @ensureExpressions(@elseBody.makeReturn()) this else new ReturnNode this diff --git a/src/rewriter.coffee b/src/rewriter.coffee index 0038115f..f3a0de69 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -198,7 +198,7 @@ exports.Rewriter = class Rewriter @scanTokens (prev, token, post, i) => for pair in pairs [open, close] = pair - levels[open] ||= 0 + levels[open] or= 0 if token[0] is open openLine[open] = token[2] if levels[open] == 0 levels[open] += 1