Add floor division `//` and modulo `%%` operators, and compound forms of the new operators

Also kill the empty regex :(
This commit is contained in:
Demian Ferreiro 2013-03-25 03:19:05 -03:00
parent 08b59aef8a
commit 22e8856b4d
6 changed files with 140 additions and 47 deletions

View File

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

View File

@ -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';
},

View File

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

View File

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

View File

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

View File

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