Skip to content

Commit 22e8856

Browse files
committed
Add floor division // and modulo %% operators, and compound forms of the new operators
Also kill the empty regex :(
1 parent 08b59ae commit 22e8856

File tree

6 files changed

+140
-47
lines changed

6 files changed

+140
-47
lines changed

lib/coffee-script/lexer.js

Lines changed: 12 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/coffee-script/nodes.js

Lines changed: 46 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lexer.coffee

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,21 +237,21 @@ exports.Lexer = class Lexer
237237
# JavaScript and Ruby.
238238
regexToken: ->
239239
return 0 if @chunk.charAt(0) isnt '/'
240-
if match = HEREGEX.exec @chunk
241-
length = @heregexToken match
242-
return length
240+
return length if length = @heregexToken()
243241

244242
prev = last @tokens
245243
return 0 if prev and (prev[0] in (if prev.spaced then NOT_REGEX else NOT_SPACED_REGEX))
246244
return 0 unless match = REGEX.exec @chunk
247245
[match, regex, flags] = match
246+
# Avoid conflicts with floor division operator.
247+
return 0 if regex is '//'
248248
if regex[..1] is '/*' then @error 'regular expressions cannot begin with `*`'
249-
if regex is '//' then regex = '/(?:)/'
250249
@token 'REGEX', "#{regex}#{flags}", 0, match.length
251250
match.length
252251

253252
# Matches multiline extended regular expressions.
254-
heregexToken: (match) ->
253+
heregexToken: ->
254+
return 0 unless match = HEREGEX.exec @chunk
255255
[heregex, body, flags] = match
256256
if 0 > body.indexOf '#{'
257257
re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/')
@@ -768,10 +768,9 @@ OPERATOR = /// ^ (
768768
| [-+*/%<>&|^!?=]= # compound assign / compare
769769
| >>>=? # zero-fill right shift
770770
| ([-+:])\1 # doubles
771-
| ([&|<>])\2=? # logic / shift
771+
| ([&|<>*/%])\2=? # logic / shift / power / floor division / modulo
772772
| \?(\.|::) # soak access
773773
| \.{2,3} # range or splat
774-
| \*\* # power
775774
) ///
776775

777776
WHITESPACE = /^[^\n\S]+/
@@ -818,7 +817,8 @@ TRAILING_SPACES = /\s+$/
818817

819818
# Compound assignment tokens.
820819
COMPOUND_ASSIGN = [
821-
'-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|='
820+
'-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>='
821+
'&=', '^=', '|=', '**=', '//=', '%%='
822822
]
823823

824824
# Unary tokens.
@@ -836,7 +836,7 @@ SHIFT = ['<<', '>>', '>>>']
836836
COMPARE = ['==', '!=', '<', '>', '<=', '>=']
837837

838838
# Mathematical tokens.
839-
MATH = ['*', '/', '%']
839+
MATH = ['*', '/', '%', '//', '%%']
840840

841841
# Relational tokens that are negatable with `not` prefix.
842842
RELATION = ['IN', 'OF', 'INSTANCEOF']

src/nodes.coffee

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,7 @@ exports.Assign = class Assign extends Base
11311131
return @compilePatternMatch o if @variable.isArray() or @variable.isObject()
11321132
return @compileSplice o if @variable.isSplice()
11331133
return @compileConditional o if @context in ['||=', '&&=', '?=']
1134+
return @compileSpecialMath o if @context in ['**=', '//=', '%%=']
11341135
compiledName = @variable.compileToFragments o, LEVEL_LIST
11351136
name = fragmentsToText compiledName
11361137
unless @context
@@ -1239,6 +1240,12 @@ exports.Assign = class Assign extends Base
12391240
if "?" in @context then o.isExistentialEquals = true
12401241
new Op(@context[...-1], left, new Assign(right, @value, '=') ).compileToFragments o
12411242

1243+
# Convert special math assignment operators like `a **= b` to the equivalent
1244+
# extended form `a = a ** b` and then compiles that.
1245+
compileSpecialMath: (o) ->
1246+
[left, right] = @variable.cacheReference o
1247+
new Assign(left, new Op(@context[...-1], right, @value)).compileToFragments o
1248+
12421249
# Compile the assignment from an array splice literal, using JavaScript's
12431250
# `Array#splice` method.
12441251
compileSplice: (o) ->
@@ -1622,11 +1629,16 @@ exports.Op = class Op extends Base
16221629
@error "cannot increment/decrement \"#{@first.unwrapAll().value}\""
16231630
return @compileUnary o if @isUnary()
16241631
return @compileChain o if isChain
1625-
return @compileExistence o if @operator is '?'
1626-
return @compilePower o if @operator is '**'
1627-
answer = [].concat @first.compileToFragments(o, LEVEL_OP), @makeCode(' ' + @operator + ' '),
1628-
@second.compileToFragments(o, LEVEL_OP)
1629-
if o.level <= LEVEL_OP then answer else @wrapInBraces answer
1632+
switch @operator
1633+
when '?' then @compileExistence o
1634+
when '**' then @compilePower o
1635+
when '//' then @compileFloorDivision o
1636+
when '%%' then @compileModulo o
1637+
else
1638+
lhs = @first.compileToFragments o, LEVEL_OP
1639+
rhs = @second.compileToFragments o, LEVEL_OP
1640+
answer = [].concat lhs, @makeCode(" #{@operator} "), rhs
1641+
if o.level <= LEVEL_OP then answer else @wrapInBraces answer
16301642

16311643
# Mimic Python's chained comparisons when multiple comparison operators are
16321644
# used sequentially. For example:
@@ -1673,6 +1685,15 @@ exports.Op = class Op extends Base
16731685
pow = new Value new Literal('Math'), [new Access new Literal 'pow']
16741686
new Call(pow, [@first, @second]).compileToFragments o
16751687

1688+
compileFloorDivision: (o) ->
1689+
floor = new Value new Literal('Math'), [new Access new Literal 'floor']
1690+
div = new Op '/', @first, @second
1691+
new Call(floor, [div]).compileToFragments o
1692+
1693+
compileModulo: (o) ->
1694+
mod = new Value new Literal utility 'modulo'
1695+
new Call(mod, [@first, @second]).compileToFragments o
1696+
16761697
toString: (idt) ->
16771698
super idt, @constructor.name + ' ' + @operator
16781699

@@ -2131,6 +2152,10 @@ UTILITIES =
21312152
[].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }
21322153
"""
21332154

2155+
modulo: -> """
2156+
function(a, b) { return (a % b + b) % b; }
2157+
"""
2158+
21342159
# Shortcuts to speed up the lookup time for native functions.
21352160
hasProp: -> '{}.hasOwnProperty'
21362161
slice : -> '[].slice'

test/operators.coffee

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,47 @@ test "power operator has higher precedence than other maths operators", ->
307307
eq 0, (!2) ** 2
308308
eq -2, ~1 ** 5
309309

310-
#test "power operator has lower precedence than"
311-
312310
test "power operator is right associative", ->
313-
eq 2, 2 ** 1 ** 3
311+
eq 2, 2 ** 1 ** 3
312+
313+
test "power operator compound assignment", ->
314+
a = 2
315+
a **= 3
316+
eq 8, a
317+
318+
test "floor division operator", ->
319+
eq 2, 7 // 3
320+
eq -3, -7 // 3
321+
eq NaN, 0 // 0
322+
323+
test "floor division operator compound assignment", ->
324+
a = 7
325+
a //= 2
326+
eq 3, a
327+
328+
test "modulo operator", ->
329+
check = (a, b, expected) ->
330+
res = a %% b
331+
# Don't use eq because it treats 0 as different to -0.
332+
ok res == expected or isNaN(res) and isNaN(expected),
333+
"expected #{a} %%%% #{b} to be #{expected}"
334+
check 0, 1, 0
335+
check 0, -1, 0
336+
check 1, 0, NaN
337+
check 1, 2, 1
338+
check 1, -2, -1
339+
check 1, 3, 1
340+
check 2, 3, 2
341+
check 3, 3, 0
342+
check 4, 3, 1
343+
check -1, 3, 2
344+
check -2, 3, 1
345+
check -3, 3, 0
346+
check -4, 3, 2
347+
check 5.5, 2.5, 0.5
348+
check -5.5, 2.5, 2.0
349+
350+
test "modulo operator compound assignment", ->
351+
a = -2
352+
a %%= 5
353+
eq 3, a

test/regexps.coffee

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,3 @@ test "an empty heregex will compile to an empty, non-capturing group", ->
5555

5656
test "#1724: regular expressions beginning with `*`", ->
5757
throws -> CoffeeScript.compile '/// * ///'
58-
59-
test "empty regular expressions with flags", ->
60-
fn = (x) -> x
61-
a = "" + //i
62-
fn ""
63-
eq '/(?:)/i', a

0 commit comments

Comments
 (0)