diff --git a/lib/coffee-script/lexer.js b/lib/coffee-script/lexer.js index 5c034083..9b95dc9b 100644 --- a/lib/coffee-script/lexer.js +++ b/lib/coffee-script/lexer.js @@ -248,8 +248,7 @@ if (this.chunk.charAt(0) !== '/') { return 0; } - if (match = HEREGEX.exec(this.chunk)) { - length = this.heregexToken(match); + if (length = this.heregexToken()) { return length; } prev = last(this.tokens); @@ -260,18 +259,21 @@ return 0; } _ref3 = match, match = _ref3[0], regex = _ref3[1], flags = _ref3[2]; + if (regex === '//') { + return 0; + } if (regex.slice(0, 2) === '/*') { this.error('regular expressions cannot begin with `*`'); } - if (regex === '//') { - regex = '/(?:)/'; - } this.token('REGEX', "" + regex + flags, 0, match.length); return match.length; }; - Lexer.prototype.heregexToken = function(match) { - var body, flags, flagsOffset, heregex, plusToken, prev, re, tag, token, tokens, value, _i, _len, _ref2, _ref3, _ref4; + Lexer.prototype.heregexToken = function() { + var body, flags, flagsOffset, heregex, match, plusToken, prev, re, tag, token, tokens, value, _i, _len, _ref2, _ref3, _ref4; + if (!(match = HEREGEX.exec(this.chunk))) { + return 0; + } heregex = match[0], body = match[1], flags = match[2]; if (0 > body.indexOf('#{')) { re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/'); @@ -832,7 +834,7 @@ HEREDOC = /^("""|''')([\s\S]*?)(?:\n[^\n\S]*)?\1/; - OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>])\2=?|\?(\.|::)|\.{2,3}|\*\*)/; + OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/; WHITESPACE = /^[^\n\S]+/; @@ -862,7 +864,7 @@ TRAILING_SPACES = /\s+$/; - COMPOUND_ASSIGN = ['-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|=']; + COMPOUND_ASSIGN = ['-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|=', '**=', '//=', '%%=']; UNARY = ['NEW', 'TYPEOF', 'DELETE', 'DO']; @@ -874,7 +876,7 @@ COMPARE = ['==', '!=', '<', '>', '<=', '>=']; - MATH = ['*', '/', '%']; + MATH = ['*', '/', '%', '//', '%%']; RELATION = ['IN', 'OF', 'INSTANCEOF']; diff --git a/lib/coffee-script/nodes.js b/lib/coffee-script/nodes.js index dcd81c7c..86ff7045 100644 --- a/lib/coffee-script/nodes.js +++ b/lib/coffee-script/nodes.js @@ -1561,7 +1561,7 @@ }; Assign.prototype.compileNode = function(o) { - var answer, compiledName, isValue, match, name, val, varBase, _ref4, _ref5, _ref6, _ref7; + var answer, compiledName, isValue, match, name, val, varBase, _ref4, _ref5, _ref6, _ref7, _ref8; if (isValue = this.variable instanceof Value) { if (this.variable.isArray() || this.variable.isObject()) { return this.compilePatternMatch(o); @@ -1572,6 +1572,9 @@ if ((_ref4 = this.context) === '||=' || _ref4 === '&&=' || _ref4 === '?=') { return this.compileConditional(o); } + if ((_ref5 = this.context) === '**=' || _ref5 === '//=' || _ref5 === '%%=') { + return this.compileSpecialMath(o); + } } compiledName = this.variable.compileToFragments(o, LEVEL_LIST); name = fragmentsToText(compiledName); @@ -1592,7 +1595,7 @@ if (match[1]) { this.value.klass = match[1]; } - this.value.name = (_ref5 = (_ref6 = (_ref7 = match[2]) != null ? _ref7 : match[3]) != null ? _ref6 : match[4]) != null ? _ref5 : match[5]; + this.value.name = (_ref6 = (_ref7 = (_ref8 = match[2]) != null ? _ref8 : match[3]) != null ? _ref7 : match[4]) != null ? _ref6 : match[5]; } val = this.value.compileToFragments(o, LEVEL_LIST); if (this.context === 'object') { @@ -1715,6 +1718,12 @@ return new Op(this.context.slice(0, -1), left, new Assign(right, this.value, '=')).compileToFragments(o); }; + Assign.prototype.compileSpecialMath = function(o) { + var left, right, _ref4; + _ref4 = this.variable.cacheReference(o), left = _ref4[0], right = _ref4[1]; + return new Assign(left, new Op(this.context.slice(0, -1), right, this.value)).compileToFragments(o); + }; + Assign.prototype.compileSplice = function(o) { var answer, exclusive, from, fromDecl, fromRef, name, to, valDef, valRef, _ref4, _ref5, _ref6; _ref4 = this.variable.properties.pop().range, from = _ref4.from, to = _ref4.to, exclusive = _ref4.exclusive; @@ -2268,7 +2277,7 @@ }; Op.prototype.compileNode = function(o) { - var answer, isChain, _ref4, _ref5; + var answer, isChain, lhs, rhs, _ref4, _ref5; isChain = this.isChainable() && this.first.isChainable(); if (!isChain) { this.first.front = this.front; @@ -2285,17 +2294,24 @@ if (isChain) { return this.compileChain(o); } - if (this.operator === '?') { - return this.compileExistence(o); - } - if (this.operator === '**') { - return this.compilePower(o); - } - answer = [].concat(this.first.compileToFragments(o, LEVEL_OP), this.makeCode(' ' + this.operator + ' '), this.second.compileToFragments(o, LEVEL_OP)); - if (o.level <= LEVEL_OP) { - return answer; - } else { - return this.wrapInBraces(answer); + switch (this.operator) { + case '?': + return this.compileExistence(o); + case '**': + return this.compilePower(o); + case '//': + return this.compileFloorDivision(o); + case '%%': + return this.compileModulo(o); + default: + lhs = this.first.compileToFragments(o, LEVEL_OP); + rhs = this.second.compileToFragments(o, LEVEL_OP); + answer = [].concat(lhs, this.makeCode(" " + this.operator + " "), rhs); + if (o.level <= LEVEL_OP) { + return answer; + } else { + return this.wrapInBraces(answer); + } } }; @@ -2353,6 +2369,19 @@ return new Call(pow, [this.first, this.second]).compileToFragments(o); }; + Op.prototype.compileFloorDivision = function(o) { + var div, floor; + floor = new Value(new Literal('Math'), [new Access(new Literal('floor'))]); + div = new Op('/', this.first, this.second); + return new Call(floor, [div]).compileToFragments(o); + }; + + Op.prototype.compileModulo = function(o) { + var mod; + mod = new Value(new Literal(utility('modulo'))); + return new Call(mod, [this.first, this.second]).compileToFragments(o); + }; + Op.prototype.toString = function(idt) { return Op.__super__.toString.call(this, idt, this.constructor.name + ' ' + this.operator); }; @@ -3005,6 +3034,9 @@ indexOf: function() { return "[].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }"; }, + modulo: function() { + return "function(a, b) { return (a % b + b) % b; }"; + }, hasProp: function() { return '{}.hasOwnProperty'; }, diff --git a/src/lexer.coffee b/src/lexer.coffee index c0a05ea4..1a9fb880 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -237,21 +237,21 @@ exports.Lexer = class Lexer # JavaScript and Ruby. regexToken: -> return 0 if @chunk.charAt(0) isnt '/' - if match = HEREGEX.exec @chunk - length = @heregexToken match - return length + return length if length = @heregexToken() prev = last @tokens return 0 if prev and (prev[0] in (if prev.spaced then NOT_REGEX else NOT_SPACED_REGEX)) return 0 unless match = REGEX.exec @chunk [match, regex, flags] = match + # Avoid conflicts with floor division operator. + return 0 if regex is '//' if regex[..1] is '/*' then @error 'regular expressions cannot begin with `*`' - if regex is '//' then regex = '/(?:)/' @token 'REGEX', "#{regex}#{flags}", 0, match.length match.length # Matches multiline extended regular expressions. - heregexToken: (match) -> + heregexToken: -> + return 0 unless match = HEREGEX.exec @chunk [heregex, body, flags] = match if 0 > body.indexOf '#{' re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/') @@ -768,10 +768,9 @@ OPERATOR = /// ^ ( | [-+*/%<>&|^!?=]= # compound assign / compare | >>>=? # zero-fill right shift | ([-+:])\1 # doubles - | ([&|<>])\2=? # logic / shift + | ([&|<>*/%])\2=? # logic / shift / power / floor division / modulo | \?(\.|::) # soak access | \.{2,3} # range or splat - | \*\* # power ) /// WHITESPACE = /^[^\n\S]+/ @@ -818,7 +817,8 @@ TRAILING_SPACES = /\s+$/ # Compound assignment tokens. COMPOUND_ASSIGN = [ - '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|=' + '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=' + '&=', '^=', '|=', '**=', '//=', '%%=' ] # Unary tokens. @@ -836,7 +836,7 @@ SHIFT = ['<<', '>>', '>>>'] COMPARE = ['==', '!=', '<', '>', '<=', '>='] # Mathematical tokens. -MATH = ['*', '/', '%'] +MATH = ['*', '/', '%', '//', '%%'] # Relational tokens that are negatable with `not` prefix. RELATION = ['IN', 'OF', 'INSTANCEOF'] diff --git a/src/nodes.coffee b/src/nodes.coffee index 8b816649..de2c800e 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1131,6 +1131,7 @@ exports.Assign = class Assign extends Base return @compilePatternMatch o if @variable.isArray() or @variable.isObject() return @compileSplice o if @variable.isSplice() return @compileConditional o if @context in ['||=', '&&=', '?='] + return @compileSpecialMath o if @context in ['**=', '//=', '%%='] compiledName = @variable.compileToFragments o, LEVEL_LIST name = fragmentsToText compiledName unless @context @@ -1239,6 +1240,12 @@ exports.Assign = class Assign extends Base if "?" in @context then o.isExistentialEquals = true new Op(@context[...-1], left, new Assign(right, @value, '=') ).compileToFragments o + # Convert special math assignment operators like `a **= b` to the equivalent + # extended form `a = a ** b` and then compiles that. + compileSpecialMath: (o) -> + [left, right] = @variable.cacheReference o + new Assign(left, new Op(@context[...-1], right, @value)).compileToFragments o + # Compile the assignment from an array splice literal, using JavaScript's # `Array#splice` method. compileSplice: (o) -> @@ -1622,11 +1629,16 @@ exports.Op = class Op extends Base @error "cannot increment/decrement \"#{@first.unwrapAll().value}\"" return @compileUnary o if @isUnary() return @compileChain o if isChain - return @compileExistence o if @operator is '?' - return @compilePower o if @operator is '**' - answer = [].concat @first.compileToFragments(o, LEVEL_OP), @makeCode(' ' + @operator + ' '), - @second.compileToFragments(o, LEVEL_OP) - if o.level <= LEVEL_OP then answer else @wrapInBraces answer + switch @operator + when '?' then @compileExistence o + when '**' then @compilePower o + when '//' then @compileFloorDivision o + when '%%' then @compileModulo o + else + lhs = @first.compileToFragments o, LEVEL_OP + rhs = @second.compileToFragments o, LEVEL_OP + answer = [].concat lhs, @makeCode(" #{@operator} "), rhs + if o.level <= LEVEL_OP then answer else @wrapInBraces answer # Mimic Python's chained comparisons when multiple comparison operators are # used sequentially. For example: @@ -1673,6 +1685,15 @@ exports.Op = class Op extends Base pow = new Value new Literal('Math'), [new Access new Literal 'pow'] new Call(pow, [@first, @second]).compileToFragments o + compileFloorDivision: (o) -> + floor = new Value new Literal('Math'), [new Access new Literal 'floor'] + div = new Op '/', @first, @second + new Call(floor, [div]).compileToFragments o + + compileModulo: (o) -> + mod = new Value new Literal utility 'modulo' + new Call(mod, [@first, @second]).compileToFragments o + toString: (idt) -> super idt, @constructor.name + ' ' + @operator @@ -2131,6 +2152,10 @@ UTILITIES = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; } """ + modulo: -> """ + function(a, b) { return (a % b + b) % b; } + """ + # Shortcuts to speed up the lookup time for native functions. hasProp: -> '{}.hasOwnProperty' slice : -> '[].slice' diff --git a/test/operators.coffee b/test/operators.coffee index c5c750dc..3008beee 100644 --- a/test/operators.coffee +++ b/test/operators.coffee @@ -307,7 +307,47 @@ test "power operator has higher precedence than other maths operators", -> eq 0, (!2) ** 2 eq -2, ~1 ** 5 -#test "power operator has lower precedence than" - test "power operator is right associative", -> - eq 2, 2 ** 1 ** 3 \ No newline at end of file + eq 2, 2 ** 1 ** 3 + +test "power operator compound assignment", -> + a = 2 + a **= 3 + eq 8, a + +test "floor division operator", -> + eq 2, 7 // 3 + eq -3, -7 // 3 + eq NaN, 0 // 0 + +test "floor division operator compound assignment", -> + a = 7 + a //= 2 + eq 3, a + +test "modulo operator", -> + check = (a, b, expected) -> + res = a %% b + # Don't use eq because it treats 0 as different to -0. + ok res == expected or isNaN(res) and isNaN(expected), + "expected #{a} %%%% #{b} to be #{expected}" + check 0, 1, 0 + check 0, -1, 0 + check 1, 0, NaN + check 1, 2, 1 + check 1, -2, -1 + check 1, 3, 1 + check 2, 3, 2 + check 3, 3, 0 + check 4, 3, 1 + check -1, 3, 2 + check -2, 3, 1 + check -3, 3, 0 + check -4, 3, 2 + check 5.5, 2.5, 0.5 + check -5.5, 2.5, 2.0 + +test "modulo operator compound assignment", -> + a = -2 + a %%= 5 + eq 3, a diff --git a/test/regexps.coffee b/test/regexps.coffee index addce005..db3d3ad9 100644 --- a/test/regexps.coffee +++ b/test/regexps.coffee @@ -55,9 +55,3 @@ test "an empty heregex will compile to an empty, non-capturing group", -> test "#1724: regular expressions beginning with `*`", -> throws -> CoffeeScript.compile '/// * ///' - -test "empty regular expressions with flags", -> - fn = (x) -> x - a = "" + //i - fn "" - eq '/(?:)/i', a