Skip to content

Commit d4fec4e

Browse files
committed
Implement numeric separators for binary integer literals
1 parent 809a893 commit d4fec4e

File tree

4 files changed

+104
-12
lines changed

4 files changed

+104
-12
lines changed

lib/Common/Common/NumberUtilities.cpp

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,21 +511,38 @@ using namespace Js;
511511
}
512512

513513
template <typename EncodedChar>
514-
double NumberUtilities::DblFromBinary(const EncodedChar *psz, const EncodedChar **ppchLim)
514+
double NumberUtilities::DblFromBinary(const EncodedChar *psz, const EncodedChar **ppchLim, bool isNumericSeparatorEnabled)
515515
{
516516
double dbl = 0;
517517
Assert(Js::NumberUtilities::LuHiDbl(dbl) == 0);
518518
Assert(Js::NumberUtilities::LuLoDbl(dbl) == 0);
519519
uint uT;
520520
byte bExtra = 0;
521521
int cbit = 0;
522+
const EncodedChar* pszSave = psz;
522523
// Skip leading zeros.
524+
LSkipZeroes:
523525
while (*psz == '0')
524526
psz++;
525527
// Get the first digit.
526528
uT = *psz - '0';
527529
if (uT > 1)
528530
{
531+
// We can skip over this numeric separator character if:
532+
// - numeric separators are enabled
533+
// - we've walked past at least one zero character (ie: this isn't the first character in psz)
534+
// - the previous character was a zero
535+
// - the following character is a valid binary digit
536+
if (*psz == '_' &&
537+
isNumericSeparatorEnabled &&
538+
pszSave < psz &&
539+
psz[-1] == '0' &&
540+
static_cast<uint>(psz[1] - '0') <= 1)
541+
{
542+
psz++;
543+
goto LSkipZeroes;
544+
}
545+
529546
*ppchLim = psz;
530547
return dbl;
531548
}
@@ -544,12 +561,14 @@ using namespace Js;
544561
// Why 52? 52 is the last explicit bit and 1 bit away from 53 (max bits of precision
545562
// for double precision floating point)
546563
const uint leftShiftValue = 52;
547-
for (; (uT = (*psz - '0')) <= 1; psz++)
564+
565+
LGetBinaryDigit:
566+
uT = (*psz - '0');
567+
if (uT <= 1)
548568
{
549569
if (cbit <= rightShiftValue)
550570
{
551571
Js::NumberUtilities::LuHiDbl(dbl) |= (uint32)uT << (rightShiftValue - cbit);
552-
553572
}
554573
else if (cbit <= leftShiftValue)
555574
{
@@ -565,6 +584,16 @@ using namespace Js;
565584
bExtra |= 1;
566585
}
567586
cbit++;
587+
psz++;
588+
goto LGetBinaryDigit;
589+
}
590+
else if (*psz == '_')
591+
{
592+
if (isNumericSeparatorEnabled && cbit > 0 && static_cast<uint>(psz[1] - '0') <= 1 && static_cast<uint>(psz[-1] - '0') <= 1)
593+
{
594+
psz++;
595+
goto LGetBinaryDigit;
596+
}
568597
}
569598
// Set the lim.
570599
*ppchLim = psz;
@@ -694,7 +723,7 @@ using namespace Js;
694723
template double NumberUtilities::StrToDbl<utf8char_t>(const utf8char_t * psz, const utf8char_t **ppchLim, Js::ScriptContext *const scriptContext);
695724
template double NumberUtilities::DblFromHex<char16>(const char16 *psz, const char16 **ppchLim);
696725
template double NumberUtilities::DblFromHex<utf8char_t>(const utf8char_t *psz, const utf8char_t **ppchLim);
697-
template double NumberUtilities::DblFromBinary<char16>(const char16 *psz, const char16 **ppchLim);
698-
template double NumberUtilities::DblFromBinary<utf8char_t>(const utf8char_t *psz, const utf8char_t **ppchLim);
726+
template double NumberUtilities::DblFromBinary<char16>(const char16 *psz, const char16 **ppchLim, bool isNumericSeparatorEnabled);
727+
template double NumberUtilities::DblFromBinary<utf8char_t>(const utf8char_t *psz, const utf8char_t **ppchLim, bool isNumericSeparatorEnabled);
699728
template double NumberUtilities::DblFromOctal<char16>(const char16 *psz, const char16 **ppchLim);
700729
template double NumberUtilities::DblFromOctal<utf8char_t>(const utf8char_t *psz, const utf8char_t **ppchLim);

lib/Common/Common/NumberUtilities.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ namespace Js
252252
template<typename EncodedChar>
253253
static double DblFromHex(const EncodedChar *psz, const EncodedChar **ppchLim);
254254
template <typename EncodedChar>
255-
static double DblFromBinary(const EncodedChar *psz, const EncodedChar **ppchLim);
255+
static double DblFromBinary(const EncodedChar *psz, const EncodedChar **ppchLim, bool isNumericSeparatorEnabled = false);
256256
template<typename EncodedChar>
257257
static double DblFromOctal(const EncodedChar *psz, const EncodedChar **ppchLim);
258258
template<typename EncodedChar>

lib/Parser/Scan.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ typename Scanner<EncodingPolicy>::EncodedCharPtr Scanner<EncodingPolicy>::FScanN
648648
case 'b':
649649
case 'B':
650650
// Binary
651-
*pdbl = Js::NumberUtilities::DblFromBinary(p + 2, &pchT);
651+
*pdbl = Js::NumberUtilities::DblFromBinary(p + 2, &pchT, m_scriptContext->GetConfig()->IsESNumericSeparatorEnabled());
652652
baseSpecifierCheck();
653653
goto LIdCheck;
654654

test/Number/NumericSeparator.js

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
77

88
var tests = [
99
{
10-
name: "Basic decimal support",
10+
name: "Basic decimal literal support",
1111
body: function () {
1212
assert.areEqual(1234, 1_234, "1234 === 1_234");
1313
assert.areEqual(1234, 1_2_3_4, "1234 === 1_2_3_4");
@@ -18,7 +18,7 @@ var tests = [
1818
}
1919
},
2020
{
21-
name: "Decimal with exponent",
21+
name: "Decimal literal with exponent",
2222
body: function () {
2323
assert.areEqual(1e100, 1e1_00, "1e100 === 1e1_00");
2424
assert.areEqual(Infinity, 1e1_0_0_0, "Infinity === 1e1_0_0_0");
@@ -28,11 +28,11 @@ var tests = [
2828
}
2929
},
3030
{
31-
name: "Decimal bad syntax",
31+
name: "Decimal literal bad syntax",
3232
body: function () {
3333
// Decimal left-part only with numeric separators
34-
assert.throws(()=>eval('1__2'), SyntaxError, "Multiple numeric separators in a row are now allowed");
35-
assert.throws(()=>eval('1_2____3'), SyntaxError, "Multiple numeric separators in a row are now allowed");
34+
assert.throws(()=>eval('1__2'), SyntaxError, "Multiple numeric separators in a row are not allowed");
35+
assert.throws(()=>eval('1_2____3'), SyntaxError, "Multiple numeric separators in a row are not allowed");
3636
assert.throws(()=>eval('1_'), SyntaxError, "Decimal may not end in a numeric separator");
3737
assert.throws(()=>eval('1__'), SyntaxError, "Decimal may not end in a numeric separator");
3838
assert.throws(()=>eval('__1'), ReferenceError, "Decimal may not begin with a numeric separator");
@@ -65,6 +65,69 @@ var tests = [
6565
assert.throws(()=>eval('1_n'), SyntaxError);
6666
}
6767
},
68+
{
69+
name: "Strings parsed as number do not support numeric separators for decimal literals",
70+
body: function () {
71+
assert.areEqual(NaN, Number('12_34'), "NaN === Number('12_34')");
72+
assert.areEqual(NaN, Number('12e3_4'), "NaN === Number('12e3_4')");
73+
assert.areEqual(NaN, Number('1234.45_67'), "NaN === Number('1234.45_67')");
74+
assert.areEqual(NaN, Number('1234.45e6_7'), "NaN === Number('1234.45e6_7')");
75+
76+
assert.areEqual(1, parseInt('1_2'), "1 === parseInt('1_2')");
77+
assert.areEqual(1, parseInt('1e2_3'), "1 === parseInt('1e2_3')");
78+
assert.areEqual(12, parseInt('12.3_4'), "1 === parseInt('12.3_4')");
79+
assert.areEqual(12, parseInt('12.34e5_6'), "1 === parseInt('12.34e5_6')");
80+
81+
assert.areEqual(1, parseFloat('1_2'), "1 === parseFloat('1_2')");
82+
assert.areEqual(1e2, parseFloat('1e2_3'), "1 === parseFloat('1e2_3')");
83+
assert.areEqual(12.3, parseFloat('12.3_4'), "1 === parseFloat('12.3_4')");
84+
assert.areEqual(12.34e5, parseFloat('12.34e5_6'), "1 === parseFloat('12.34e5_6')");
85+
}
86+
},
87+
{
88+
name: "Basic binary literal support",
89+
body: function () {
90+
assert.areEqual(0b00, 0b0_0, "0b00 === 0b0_0");
91+
assert.areEqual(0b11, 0b1_1, "0b11 === 0b1_1");
92+
assert.areEqual(0b10, 0b1_0, "0b10 === 0b1_0");
93+
assert.areEqual(0b01, 0b0_1, "0b01 === 0b0_1");
94+
assert.areEqual(0b0001, 0b000_1, "0b0001 === 0b000_1");
95+
assert.areEqual(0b0000, 0b000_0, "0b0000 === 0b000_0");
96+
assert.areEqual(0b000011110000, 0b0000_1111_0000, "0b000011110000 === 0b0000_1111_0000");
97+
}
98+
},
99+
{
100+
name: "Binary literal bad syntax",
101+
body: function () {
102+
assert.throws(()=>eval('0b_'), SyntaxError, "'_' cannot immediately follow 0b");
103+
assert.throws(()=>eval('0b__'), SyntaxError, "'_' cannot immediately follow 0b");
104+
assert.throws(()=>eval('0b_1'), SyntaxError, "Binary literal may not begin with numeric separator");
105+
assert.throws(()=>eval('0b_0'), SyntaxError, "Binary literal may not begin with numeric separator");
106+
assert.throws(()=>eval('0b__1'), SyntaxError, "Binary literal may not begin with numeric separator");
107+
assert.throws(()=>eval('0b__0'), SyntaxError, "Binary literal may not begin with numeric separator");
108+
assert.throws(()=>eval('0b1_'), SyntaxError, "Binary literal may not end with numeric separator");
109+
assert.throws(()=>eval('0b0_'), SyntaxError, "Binary literal may not end with numeric separator");
110+
assert.throws(()=>eval('0b1__'), SyntaxError, "Binary literal may not end with numeric separator");
111+
assert.throws(()=>eval('0b0__'), SyntaxError, "Binary literal may not end with numeric separator");
112+
assert.throws(()=>eval('0b1__1'), SyntaxError, "Multiple numeric separator characters may not follow each other");
113+
assert.throws(()=>eval('0b0__0'), SyntaxError, "Multiple numeric separator characters may not follow each other");
114+
assert.throws(()=>eval('0b000__1'), SyntaxError, "Multiple numeric separator characters may not follow each other");
115+
assert.throws(()=>eval('0b000_a'), SyntaxError, "After initial zeroes, a numeric separator followed by an invalid character");
116+
}
117+
},
118+
{
119+
name: "Strings parsed as number do not support numeric separators for binary literals",
120+
body: function () {
121+
assert.areEqual(NaN, Number('0b0_1'), "NaN === Number('0b0_1')");
122+
assert.areEqual(NaN, Number('0b1_0'), "NaN === Number('0b1_0')");
123+
assert.areEqual(NaN, Number('0b0_0'), "NaN === Number('0b0_0')");
124+
assert.areEqual(NaN, Number('0b1_1'), "NaN === Number('0b1_1')");
125+
126+
assert.areEqual(0, parseInt('0b1_0'), "0 === parseInt('0b1_0')");
127+
128+
assert.areEqual(0, parseFloat('0b1_0'), "0 === parseFloat('0b1_0')");
129+
}
130+
},
68131
];
69132

70133
testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });

0 commit comments

Comments
 (0)