mirror of
https://github.com/jashkenas/coffeescript.git
synced 2022-11-09 12:23:24 -05:00
Define proper operator precedence for bitwise/logical operators
This is an upstream port for the patch https://github.com/decaffeinate/coffeescript/pull/8 See https://github.com/decaffeinate/decaffeinate/issues/291 for the bug that this fixed. For the most part, CoffeeScript and JavaScript have the same precedence rules, but in some cases, the intermediate AST format didn't represent the actual evaluation order. For example, in the expression `a or b and c`, the `and` is evaluated first, but the parser treated the two operators with equal precedence. This was still correct end-to-end because CoffeeScript simply emitted the result without parens, but any intermediate tools using the CoffeeScript parser could become confused. Here are the JS operator precedence rules: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence For the most part, CoffeeScript already follows these. `COMPARE` operators already behave differently due to chained comparisons, so I think we don't need to worry about following JS precedence for those. So I think the only case where it was behaving differently in an important way was for the binary/bitwise operators that are being changed here. As part of this change, I also introduced a new token tag, `BIN?`, for the binary form of the `?` operator.
This commit is contained in:
parent
11561dddcf
commit
e14946b3e6
6 changed files with 87 additions and 31 deletions
|
@ -724,7 +724,17 @@
|
|||
return new Op($2, $1, $3);
|
||||
}), o('Expression COMPARE Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression LOGIC Expression', function() {
|
||||
}), o('Expression & Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression ^ Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression | Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression && Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression || Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression BIN? Expression', function() {
|
||||
return new Op($2, $1, $3);
|
||||
}), o('Expression RELATION Expression', function() {
|
||||
if ($2.charAt(0) === '!') {
|
||||
|
@ -744,7 +754,7 @@
|
|||
]
|
||||
};
|
||||
|
||||
operators = [['left', '.', '?.', '::', '?::'], ['left', 'CALL_START', 'CALL_END'], ['nonassoc', '++', '--'], ['left', '?'], ['right', 'UNARY'], ['right', '**'], ['right', 'UNARY_MATH'], ['left', 'MATH'], ['left', '+', '-'], ['left', 'SHIFT'], ['left', 'RELATION'], ['left', 'COMPARE'], ['left', 'LOGIC'], ['nonassoc', 'INDENT', 'OUTDENT'], ['right', 'YIELD'], ['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS'], ['right', 'FORIN', 'FOROF', 'BY', 'WHEN'], ['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS', 'IMPORT', 'EXPORT'], ['left', 'POST_IF']];
|
||||
operators = [['left', '.', '?.', '::', '?::'], ['left', 'CALL_START', 'CALL_END'], ['nonassoc', '++', '--'], ['left', '?'], ['right', 'UNARY'], ['right', '**'], ['right', 'UNARY_MATH'], ['left', 'MATH'], ['left', '+', '-'], ['left', 'SHIFT'], ['left', 'RELATION'], ['left', 'COMPARE'], ['left', '&'], ['left', '^'], ['left', '|'], ['left', '&&'], ['left', '||'], ['left', 'BIN?'], ['nonassoc', 'INDENT', 'OUTDENT'], ['right', 'YIELD'], ['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS'], ['right', 'FORIN', 'FOROF', 'BY', 'WHEN'], ['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS', 'IMPORT', 'EXPORT'], ['left', 'POST_IF']];
|
||||
|
||||
tokens = [];
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Generated by CoffeeScript 1.11.1
|
||||
(function() {
|
||||
var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INVALID_ESCAPE, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, LOGIC, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isUnassignable, key, locationDataToString, ref, ref1, repeat, starts, throwSyntaxError,
|
||||
var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INVALID_ESCAPE, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isUnassignable, key, locationDataToString, ref, ref1, repeat, starts, throwSyntaxError,
|
||||
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
|
||||
slice = [].slice;
|
||||
|
||||
|
@ -144,9 +144,6 @@
|
|||
case '==':
|
||||
case '!=':
|
||||
return 'COMPARE';
|
||||
case '&&':
|
||||
case '||':
|
||||
return 'LOGIC';
|
||||
case 'true':
|
||||
case 'false':
|
||||
return 'BOOL';
|
||||
|
@ -154,6 +151,9 @@
|
|||
case 'continue':
|
||||
case 'debugger':
|
||||
return 'STATEMENT';
|
||||
case '&&':
|
||||
case '||':
|
||||
return id;
|
||||
default:
|
||||
return tag;
|
||||
}
|
||||
|
@ -572,8 +572,8 @@
|
|||
tag = 'UNARY_MATH';
|
||||
} else if (indexOf.call(SHIFT, value) >= 0) {
|
||||
tag = 'SHIFT';
|
||||
} else if (indexOf.call(LOGIC, value) >= 0 || value === '?' && (prev != null ? prev.spaced : void 0)) {
|
||||
tag = 'LOGIC';
|
||||
} else if (value === '?' && (prev != null ? prev.spaced : void 0)) {
|
||||
tag = 'BIN?';
|
||||
} else if (prev && !prev.spaced) {
|
||||
if (value === '(' && (ref5 = prev[0], indexOf.call(CALLABLE, ref5) >= 0)) {
|
||||
if (prev[0] === '?') {
|
||||
|
@ -835,7 +835,7 @@
|
|||
|
||||
Lexer.prototype.unfinished = function() {
|
||||
var ref2;
|
||||
return LINE_CONTINUER.test(this.chunk) || ((ref2 = this.tag()) === '\\' || ref2 === '.' || ref2 === '?.' || ref2 === '?::' || ref2 === 'UNARY' || ref2 === 'MATH' || ref2 === 'UNARY_MATH' || ref2 === '+' || ref2 === '-' || ref2 === '**' || ref2 === 'SHIFT' || ref2 === 'RELATION' || ref2 === 'COMPARE' || ref2 === 'LOGIC' || ref2 === 'THROW' || ref2 === 'EXTENDS');
|
||||
return LINE_CONTINUER.test(this.chunk) || ((ref2 = this.tag()) === '\\' || ref2 === '.' || ref2 === '?.' || ref2 === '?::' || ref2 === 'UNARY' || ref2 === 'MATH' || ref2 === 'UNARY_MATH' || ref2 === '+' || ref2 === '-' || ref2 === '**' || ref2 === 'SHIFT' || ref2 === 'RELATION' || ref2 === 'COMPARE' || ref2 === '&' || ref2 === '^' || ref2 === '|' || ref2 === '&&' || ref2 === '||' || ref2 === 'BIN?' || ref2 === 'THROW' || ref2 === 'EXTENDS');
|
||||
};
|
||||
|
||||
Lexer.prototype.formatString = function(str) {
|
||||
|
@ -1041,8 +1041,6 @@
|
|||
|
||||
UNARY_MATH = ['!', '~'];
|
||||
|
||||
LOGIC = ['&&', '||', '&', '|', '^'];
|
||||
|
||||
SHIFT = ['<<', '>>', '>>>'];
|
||||
|
||||
COMPARE = ['==', '!=', '<', '>', '<=', '>='];
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -660,7 +660,12 @@ grammar =
|
|||
o 'Expression ** Expression', -> new Op $2, $1, $3
|
||||
o 'Expression SHIFT Expression', -> new Op $2, $1, $3
|
||||
o 'Expression COMPARE Expression', -> new Op $2, $1, $3
|
||||
o 'Expression LOGIC Expression', -> new Op $2, $1, $3
|
||||
o 'Expression & Expression', -> new Op $2, $1, $3
|
||||
o 'Expression ^ Expression', -> new Op $2, $1, $3
|
||||
o 'Expression | Expression', -> new Op $2, $1, $3
|
||||
o 'Expression && Expression', -> new Op $2, $1, $3
|
||||
o 'Expression || Expression', -> new Op $2, $1, $3
|
||||
o 'Expression BIN? Expression', -> new Op $2, $1, $3
|
||||
o 'Expression RELATION Expression', ->
|
||||
if $2.charAt(0) is '!'
|
||||
new Op($2[1..], $1, $3).invert()
|
||||
|
@ -701,7 +706,12 @@ operators = [
|
|||
['left', 'SHIFT']
|
||||
['left', 'RELATION']
|
||||
['left', 'COMPARE']
|
||||
['left', 'LOGIC']
|
||||
['left', '&']
|
||||
['left', '^']
|
||||
['left', '|']
|
||||
['left', '&&']
|
||||
['left', '||']
|
||||
['left', 'BIN?']
|
||||
['nonassoc', 'INDENT', 'OUTDENT']
|
||||
['right', 'YIELD']
|
||||
['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS']
|
||||
|
|
|
@ -170,10 +170,10 @@ exports.Lexer = class Lexer
|
|||
tag = switch id
|
||||
when '!' then 'UNARY'
|
||||
when '==', '!=' then 'COMPARE'
|
||||
when '&&', '||' then 'LOGIC'
|
||||
when 'true', 'false' then 'BOOL'
|
||||
when 'break', 'continue', \
|
||||
'debugger' then 'STATEMENT'
|
||||
when '&&', '||' then id
|
||||
else tag
|
||||
|
||||
tagToken = @token tag, id, 0, idLength
|
||||
|
@ -467,7 +467,7 @@ exports.Lexer = class Lexer
|
|||
else if value in UNARY then tag = 'UNARY'
|
||||
else if value in UNARY_MATH then tag = 'UNARY_MATH'
|
||||
else if value in SHIFT then tag = 'SHIFT'
|
||||
else if value in LOGIC or value is '?' and prev?.spaced then tag = 'LOGIC'
|
||||
else if value is '?' and prev?.spaced then tag = 'BIN?'
|
||||
else if prev and not prev.spaced
|
||||
if value is '(' and prev[0] in CALLABLE
|
||||
prev[0] = 'FUNC_EXIST' if prev[0] is '?'
|
||||
|
@ -726,7 +726,8 @@ exports.Lexer = class Lexer
|
|||
unfinished: ->
|
||||
LINE_CONTINUER.test(@chunk) or
|
||||
@tag() in ['\\', '.', '?.', '?::', 'UNARY', 'MATH', 'UNARY_MATH', '+', '-',
|
||||
'**', 'SHIFT', 'RELATION', 'COMPARE', 'LOGIC', 'THROW', 'EXTENDS']
|
||||
'**', 'SHIFT', 'RELATION', 'COMPARE', '&', '^', '|', '&&', '||',
|
||||
'BIN?', 'THROW', 'EXTENDS']
|
||||
|
||||
formatString: (str) ->
|
||||
str.replace STRING_OMIT, '$1'
|
||||
|
@ -954,9 +955,6 @@ UNARY = ['NEW', 'TYPEOF', 'DELETE', 'DO']
|
|||
|
||||
UNARY_MATH = ['!', '~']
|
||||
|
||||
# Logical tokens.
|
||||
LOGIC = ['&&', '||', '&', '|', '^']
|
||||
|
||||
# Bit-shifting tokens.
|
||||
SHIFT = ['<<', '>>', '>>>']
|
||||
|
||||
|
|
40
test/parser.coffee
Normal file
40
test/parser.coffee
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Parser
|
||||
# ---------
|
||||
|
||||
test "operator precedence for logical operators", ->
|
||||
source = '''
|
||||
a or b and c
|
||||
'''
|
||||
block = CoffeeScript.nodes source
|
||||
[expression] = block.expressions
|
||||
eq expression.first.base.value, 'a'
|
||||
eq expression.operator, '||'
|
||||
eq expression.second.first.base.value, 'b'
|
||||
eq expression.second.operator, '&&'
|
||||
eq expression.second.second.base.value, 'c'
|
||||
|
||||
test "operator precedence for bitwise operators", ->
|
||||
source = '''
|
||||
a | b ^ c & d
|
||||
'''
|
||||
block = CoffeeScript.nodes source
|
||||
[expression] = block.expressions
|
||||
eq expression.first.base.value, 'a'
|
||||
eq expression.operator, '|'
|
||||
eq expression.second.first.base.value, 'b'
|
||||
eq expression.second.operator, '^'
|
||||
eq expression.second.second.first.base.value, 'c'
|
||||
eq expression.second.second.operator, '&'
|
||||
eq expression.second.second.second.base.value, 'd'
|
||||
|
||||
test "operator precedence for binary ? operator", ->
|
||||
source = '''
|
||||
a ? b and c
|
||||
'''
|
||||
block = CoffeeScript.nodes source
|
||||
[expression] = block.expressions
|
||||
eq expression.first.base.value, 'a'
|
||||
eq expression.operator, '?'
|
||||
eq expression.second.first.base.value, 'b'
|
||||
eq expression.second.operator, '&&'
|
||||
eq expression.second.second.base.value, 'c'
|
Loading…
Add table
Add a link
Reference in a new issue