diff --git a/Cakefile b/Cakefile index 217518c0..fe8f5d63 100644 --- a/Cakefile +++ b/Cakefile @@ -472,6 +472,7 @@ runTests = (CoffeeScript) -> skipUnless 'var a = 2 ** 2; a **= 3', ['exponentiation.coffee'] skipUnless 'var {...a} = {}', ['object_rest_spread.coffee'] skipUnless '/foo.bar/s.test("foo\tbar")', ['regex_dotall.coffee'] + skipUnless '1_2_3', ['numeric_literal_separators.coffee'] skipUnless '1n', ['numbers_bigint.coffee'] files = fs.readdirSync('test').filter (filename) -> filename not in testFilesToSkip diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index b718e8ba..717c1aac 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -1570,7 +1570,9 @@ // Is this an attribute with a value? }(?:\\s*:\\s*${CSX_IDENTIFIER_PART})?)([^\\S]*=(?!=))?`); - NUMBER = /^0b[01]+n?|^0o[0-7]+n?|^0x[\da-f]+n?|^\d+n|^\d*\.?\d+(?:e[+-]?\d+)?/i; // binary + // decimal without support for numeric literal separators for reference: + // \d*\.?\d+ (?:e[+-]?\d+)? + NUMBER = /^0b[01](?:_?[01])*n?|^0o[0-7](?:_?[0-7])*n?|^0x[\da-f](?:_?[\da-f])*n?|^\d+n|^(?:\d(?:_?\d)*)?\.?(?:\d(?:_?\d)*)+(?:e[+-]?(?:\d(?:_?\d)*)+)?/i; // binary // octal // hex // decimal bigint diff --git a/src/lexer.coffee b/src/lexer.coffee index 8a054391..0d067212 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -1212,11 +1212,14 @@ CSX_ATTRIBUTE = /// ^ /// NUMBER = /// - ^ 0b[01]+n? | # binary - ^ 0o[0-7]+n? | # octal - ^ 0x[\da-f]+n? | # hex - ^ \d+n | # decimal bigint - ^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal + ^ 0b[01](?:_?[01])*n? | # binary + ^ 0o[0-7](?:_?[0-7])*n? | # octal + ^ 0x[\da-f](?:_?[\da-f])*n? | # hex + ^ \d+n | # decimal bigint + ^ (?:\d(?:_?\d)*)? \.? (?:\d(?:_?\d)*)+ # decimal + (?:e[+-]? (?:\d(?:_?\d)*)+ )? + # decimal without support for numeric literal separators for reference: + # \d*\.?\d+ (?:e[+-]?\d+)? ///i OPERATOR = /// ^ ( diff --git a/test/numeric_literal_separators.coffee b/test/numeric_literal_separators.coffee new file mode 100644 index 00000000..2b1f3389 --- /dev/null +++ b/test/numeric_literal_separators.coffee @@ -0,0 +1,55 @@ +# Numeric Literal Separators +# -------------------------- + +test 'integer literals with separators', -> + eq 123_456, 123456 + eq 12_34_56, 123456 + +test 'decimal literals with separators', -> + eq 1_2.34_5, 12.345 + eq 1_0e1_0, 10e10 + eq 1_2.34_5e6_7, 12.345e67 + +test 'hexadecimal literals with separators', -> + eq 0x1_2_3_4, 0x1234 + +test 'binary literals with separators', -> + eq 0b10_10, 0b1010 + +test 'octal literals with separators', -> + eq 0o7_7_7, 0o777 + +test 'property access on a number', -> + # Somehow, `3..toFixed()` is valid JavaScript; though just `3.toFixed()` + # is not. CoffeeScript has long allowed code like `3.toFixed()` to compile + # into `3..toFixed()`. + eq 3.toFixed(), '3' + # Where this can conflict with numeric literal separators is when the + # property name contains an underscore. + Number::_23 = _23 = 'x' + eq 1._23, 'x' + ok 1._34 is undefined + delete Number::_23 + +test 'invalid decimal literal separators do not compile', -> + # `1._23` is a valid property access (see previous test) + throws -> CoffeeScript.compile '1_.23' + throws -> CoffeeScript.compile '1e_2' + throws -> CoffeeScript.compile '1e2_' + throws -> CoffeeScript.compile '1_' + throws -> CoffeeScript.compile '1__2' + +test 'invalid hexadecimal literal separators do not compile', -> + throws -> CoffeeScript.compile '0x_1234' + throws -> CoffeeScript.compile '0x1234_' + throws -> CoffeeScript.compile '0x1__34' + +test 'invalid binary literal separators do not compile', -> + throws -> CoffeeScript.compile '0b_100' + throws -> CoffeeScript.compile '0b100_' + throws -> CoffeeScript.compile '0b1__1' + +test 'invalid octal literal separators do not compile', -> + throws -> CoffeeScript.compile '0o_777' + throws -> CoffeeScript.compile '0o777_' + throws -> CoffeeScript.compile '0o6__6'