Skip to content

Commit 809a893

Browse files
committed
Implement Numeric Separator
Add support for numeric separator characters in numeric literals. These are just syntactic sugar which improve readability of numeric constants in source. They desugar completely out of the numbers when parsed. Numeric digits in the constant may have a single '_' character between them. The constant may not begin or end with a '_' character and there may not be multiple numeric separators in a row. All numeric constants are supported. Includes decimal, hex, octal and binary numeric constants. ```javascript 1234 === 1_2_3_4; // true 12.34e56 === 1_2.3_4e5_6; // true 0xff === 0xf_f; // true 0o17 === 0o1_7; // true 0b11 === 0b1_1; // true ``` Numeric Separator proposal is in stage 3 and implemented by JSC and V8. See proposal: https://github.com/tc39/proposal-numeric-separator Fixes: #6060
1 parent 1481d95 commit 809a893

File tree

9 files changed

+162
-15
lines changed

9 files changed

+162
-15
lines changed

bin/NativeTests/BigUIntTest.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Js
2626
}
2727

2828
template <typename EncodedChar>
29-
double Js::NumberUtilities::StrToDbl(const EncodedChar *, const EncodedChar **, LikelyNumberType& , bool)
29+
double Js::NumberUtilities::StrToDbl(const EncodedChar *, const EncodedChar **, LikelyNumberType& , bool, bool)
3030
{
3131
Assert(false);
3232
return 0.0;// don't care

lib/Common/Common/NumberUtilities.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ namespace Js
227227

228228
// Implemented in lib\parser\common. Should move to lib\common
229229
template<typename EncodedChar>
230-
static double StrToDbl(const EncodedChar *psz, const EncodedChar **ppchLim, LikelyNumberType& likelyType, bool isESBigIntEnabled = false);
230+
static double StrToDbl(const EncodedChar *psz, const EncodedChar **ppchLim, LikelyNumberType& likelyType, bool isESBigIntEnabled = false, bool isNumericSeparatorEnabled = false);
231231

232232
static BOOL FDblToStr(double dbl, __out_ecount(nDstBufSize) char16 *psz, int nDstBufSize);
233233
static int FDblToStr(double dbl, NumberUtilities::FormatType ft, int nDigits, __out_ecount(cchDst) char16 *pchDst, int cchDst);

lib/Common/Common/NumberUtilities_strtod.cpp

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ void BIGNUM::SetFromRgchExp(const EncodedChar *prgch, int32 cch, int32 lwExp)
366366

367367
while (++prgch < pchLim)
368368
{
369-
if (*prgch == '.')
369+
if (*prgch == '.' || *prgch == '_')
370370
continue;
371371
Assert(Js::NumberUtilities::IsDigit(*prgch));
372372
MulTenAdd((byte) (*prgch - '0'), &luExtra);
@@ -893,7 +893,7 @@ static double AdjustDbl(double dbl, const EncodedChar *prgch, int32 cch, int32 l
893893
String to Double.
894894
***************************************************************************/
895895
template <typename EncodedChar>
896-
double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar **ppchLim, LikelyNumberType& likelyNumberType, bool isBigIntEnabled)
896+
double Js::NumberUtilities::StrToDbl(const EncodedChar *psz, const EncodedChar **ppchLim, LikelyNumberType& likelyNumberType, bool isBigIntEnabled, bool isNumericSeparatorEnabled)
897897
{
898898
uint32 lu;
899899
BIGNUM num;
@@ -909,6 +909,10 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
909909
Assert(Js::NumberUtilities::IsNan(dblLowPrec));
910910
#endif //DBG
911911

912+
// Numeric separator characters exist in the numeric constant and should
913+
// be ignored.
914+
bool hasNumericSeparators = false;
915+
912916
// For the mantissa digits. After leaving the state machine, pchMinDig
913917
// points to the first digit and pchLimDig points just past the last
914918
// digit. cchDig is the number of digits. pchLimDig - pchMinDig may be
@@ -979,8 +983,11 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
979983
if (Js::NumberUtilities::IsDigit(*pch))
980984
{
981985
LGetLeftDig:
982-
pchMinDig = pch;
983-
for (cchDig = 1; Js::NumberUtilities::IsDigit(*++pch); cchDig++)
986+
if (pchMinDig == NULL)
987+
{
988+
pchMinDig = pch;
989+
}
990+
for (cchDig++; Js::NumberUtilities::IsDigit(*++pch); cchDig++)
984991
;
985992
}
986993
switch (*pch)
@@ -995,6 +1002,26 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
9951002
{
9961003
goto LBigInt;
9971004
}
1005+
goto LGetLeftDefault;
1006+
case '_':
1007+
if (isNumericSeparatorEnabled)
1008+
{
1009+
// A numeric separator is only valid if it appears between two
1010+
// digits. If the preceeding or following character is not a digit,
1011+
// we should just fallthrough and fail. Otherwise we would have to
1012+
// handle cases like 1_.0 manually above.
1013+
// cchDig holds the count of digits in the literal. If it's >0, we
1014+
// can be sure the previous pch is valid.
1015+
if (cchDig > 0 && Js::NumberUtilities::IsDigit(*(pch - 1))
1016+
&& Js::NumberUtilities::IsDigit(*(pch + 1)))
1017+
{
1018+
hasNumericSeparators = true;
1019+
pch++;
1020+
goto LGetLeftDig;
1021+
}
1022+
}
1023+
// Fallthrough
1024+
LGetLeftDefault:
9981025
default:
9991026
likelyNumberType = LikelyNumberType::Int;
10001027
}
@@ -1010,6 +1037,7 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
10101037
lwAdj--;
10111038
pchMinDig = pch;
10121039
}
1040+
LGetRightDigit:
10131041
for( ; Js::NumberUtilities::IsDigit(*pch); pch++)
10141042
{
10151043
cchDig++;
@@ -1020,6 +1048,17 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
10201048
case 'E':
10211049
case 'e':
10221050
goto LGetExp;
1051+
case '_':
1052+
if (isNumericSeparatorEnabled)
1053+
{
1054+
if (cchDig > 0 && Js::NumberUtilities::IsDigit(*(pch - 1)) &&
1055+
Js::NumberUtilities::IsDigit(*(pch + 1)))
1056+
{
1057+
hasNumericSeparators = true;
1058+
pch++;
1059+
goto LGetRightDigit;
1060+
}
1061+
}
10231062
}
10241063
goto LEnd;
10251064

@@ -1050,6 +1089,19 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
10501089
if (lwExp > 100000000)
10511090
lwExp = 100000000;
10521091
}
1092+
switch (*pch)
1093+
{
1094+
case '_':
1095+
if (isNumericSeparatorEnabled)
1096+
{
1097+
if (Js::NumberUtilities::IsDigit(*(pch - 1)) &&
1098+
Js::NumberUtilities::IsDigit(*(pch + 1)))
1099+
{
1100+
pch++;
1101+
goto LGetExpDigits;
1102+
}
1103+
}
1104+
}
10531105
goto LEnd;
10541106

10551107
LBigInt:
@@ -1070,7 +1122,8 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
10701122
pchLimDig = pch;
10711123
Assert(pchMinDig != NULL);
10721124
Assert(pchLimDig - pchMinDig == cchDig ||
1073-
pchLimDig - pchMinDig == cchDig + 1);
1125+
pchLimDig - pchMinDig == cchDig + 1 ||
1126+
(isNumericSeparatorEnabled && hasNumericSeparators));
10741127

10751128
// Limit to kcchMaxSig digits.
10761129
if (cchDig > kcchMaxSig)
@@ -1116,15 +1169,16 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
11161169
cchDig--;
11171170
lwAdj++;
11181171
}
1119-
else if (*pchLimDig != '.')
1172+
else if (*pchLimDig != '.' && *pchLimDig != '_')
11201173
{
11211174
Assert(FNzDigit(*pchLimDig));
11221175
pchLimDig++;
11231176
break;
11241177
}
11251178
}
11261179
Assert(pchLimDig - pchMinDig == cchDig ||
1127-
pchLimDig - pchMinDig == cchDig + 1);
1180+
pchLimDig - pchMinDig == cchDig + 1 ||
1181+
(isNumericSeparatorEnabled && hasNumericSeparators));
11281182

11291183
if (signExp < 0)
11301184
lwExp = -lwExp;
@@ -1139,8 +1193,14 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
11391193
// Can use the ALU.
11401194
for (lu = 0, pch = pchMinDig; pch < pchLimDig; pch++)
11411195
{
1142-
if (*pch != '.')
1196+
switch (*pch)
11431197
{
1198+
case '.':
1199+
break;
1200+
case '_':
1201+
Assert(isNumericSeparatorEnabled && hasNumericSeparators);
1202+
break;
1203+
default:
11441204
Assert(Js::NumberUtilities::IsDigit(*pch));
11451205
lu = lu * 10 + (*pch - '0');
11461206
}
@@ -1151,8 +1211,14 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
11511211
{
11521212
for (dbl = 0, pch = pchMinDig; pch < pchLimDig; pch++)
11531213
{
1154-
if (*pch != '.')
1214+
switch (*pch)
11551215
{
1216+
case '.':
1217+
break;
1218+
case '_':
1219+
Assert(isNumericSeparatorEnabled && hasNumericSeparators);
1220+
break;
1221+
default:
11561222
Assert(Js::NumberUtilities::IsDigit(*pch));
11571223
dbl = dbl * 10 + (*pch - '0');
11581224
}
@@ -1270,8 +1336,8 @@ double Js::NumberUtilities::StrToDbl( const EncodedChar *psz, const EncodedChar
12701336
return dbl;
12711337
}
12721338

1273-
template double Js::NumberUtilities::StrToDbl<char16>( const char16 * psz, const char16 **ppchLim, LikelyNumberType& likelyInt, bool isBigIntEnabled );
1274-
template double Js::NumberUtilities::StrToDbl<utf8char_t>(const utf8char_t * psz, const utf8char_t **ppchLim, LikelyNumberType& likelyInt, bool isBigIntEnabled );
1339+
template double Js::NumberUtilities::StrToDbl<char16>( const char16 * psz, const char16 **ppchLim, LikelyNumberType& likelyInt, bool isBigIntEnabled, bool isNumericSeparatorEnabled );
1340+
template double Js::NumberUtilities::StrToDbl<utf8char_t>(const utf8char_t * psz, const utf8char_t **ppchLim, LikelyNumberType& likelyInt, bool isBigIntEnabled, bool isNumericSeparatorEnabled );
12751341

12761342
/***************************************************************************
12771343
Uses big integer arithmetic to get the sequence of digits.

lib/Common/ConfigFlagsList.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ PHASE(All)
679679
#define DEFAULT_CONFIG_ES6RegExSticky (true)
680680
#define DEFAULT_CONFIG_ES2018RegExDotAll (true)
681681
#define DEFAULT_CONFIG_ESBigInt (false)
682+
#define DEFAULT_CONFIG_ESNumericSeparator (true)
682683
#define DEFAULT_CONFIG_ESSymbolDescription (true)
683684
#define DEFAULT_CONFIG_ESGlobalThis (true)
684685
#ifdef COMPILE_DISABLE_ES6RegExPrototypeProperties
@@ -1215,6 +1216,9 @@ FLAGR(Boolean, WinRTAdaptiveApps , "Enable the adaptive apps feature, all
12151216
// ES BigInt flag
12161217
FLAGR(Boolean, ESBigInt, "Enable ESBigInt flag", DEFAULT_CONFIG_ESBigInt)
12171218

1219+
// ES Numeric Separator support for numeric constants
1220+
FLAGR(Boolean, ESNumericSeparator, "Enable Numeric Separator flag", DEFAULT_CONFIG_ESNumericSeparator)
1221+
12181222
// ES Symbol.prototype.description flag
12191223
FLAGR(Boolean, ESSymbolDescription, "Enable Symbol.prototype.description", DEFAULT_CONFIG_ESSymbolDescription)
12201224

lib/Common/DataStructures/BigUInt.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ namespace Js
125125
luMul = 1;
126126
for (*pcchDig = cch; prgch < pchLim; prgch++)
127127
{
128-
if (*prgch == '.')
128+
if (*prgch == '.' || *prgch == '_')
129129
{
130130
(*pcchDig)--;
131131
continue;

lib/Parser/Scan.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ typename Scanner<EncodingPolicy>::EncodedCharPtr Scanner<EncodingPolicy>::FScanN
679679
else
680680
{
681681
LFloat:
682-
*pdbl = Js::NumberUtilities::StrToDbl(p, &pchT, likelyType, m_scriptContext->GetConfig()->IsESBigIntEnabled());
682+
*pdbl = Js::NumberUtilities::StrToDbl(p, &pchT, likelyType, m_scriptContext->GetConfig()->IsESBigIntEnabled(), m_scriptContext->GetConfig()->IsESNumericSeparatorEnabled());
683683
Assert(pchT == p || !Js::NumberUtilities::IsNan(*pdbl));
684684
if (likelyType == LikelyNumberType::BigInt)
685685
{

lib/Runtime/Base/ThreadConfigFlagsList.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ FLAG_RELEASE(IsESObjectGetOwnPropertyDescriptorsEnabled, ESObjectGetOwnPropertyD
4848
FLAG_RELEASE(IsESSharedArrayBufferEnabled, ESSharedArrayBuffer)
4949
FLAG_RELEASE(IsESDynamicImportEnabled, ESDynamicImport)
5050
FLAG_RELEASE(IsESBigIntEnabled, ESBigInt)
51+
FLAG_RELEASE(IsESNumericSeparatorEnabled, ESNumericSeparator)
5152
FLAG_RELEASE(IsESExportNsAsEnabled, ESExportNsAs)
5253
FLAG_RELEASE(IsESSymbolDescriptionEnabled, ESSymbolDescription)
5354
FLAG_RELEASE(IsESGlobalThisEnabled, ESGlobalThis)

test/Number/NumericSeparator.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//-------------------------------------------------------------------------------------------------------
2+
// Copyright (C) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
4+
//-------------------------------------------------------------------------------------------------------
5+
6+
WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
7+
8+
var tests = [
9+
{
10+
name: "Basic decimal support",
11+
body: function () {
12+
assert.areEqual(1234, 1_234, "1234 === 1_234");
13+
assert.areEqual(1234, 1_2_3_4, "1234 === 1_2_3_4");
14+
assert.areEqual(1234.567, 1_2_3_4.5_6_7, "1234.567 === 1_2_3_4.5_6_7");
15+
16+
assert.areEqual(-1234, -1_2_34, "-1234 === -1_2_34");
17+
assert.areEqual(-12.34, -1_2.3_4, "-12.34 === -1_2.3_4");
18+
}
19+
},
20+
{
21+
name: "Decimal with exponent",
22+
body: function () {
23+
assert.areEqual(1e100, 1e1_00, "1e100 === 1e1_00");
24+
assert.areEqual(Infinity, 1e1_0_0_0, "Infinity === 1e1_0_0_0");
25+
26+
assert.areEqual(123.456e23, 1_2_3.4_5_6e2_3, "123.456e23 === 1_2_3.4_5_6e2_3");
27+
assert.areEqual(123.456e001, 1_2_3.4_5_6e0_0_1, "123.456e001 === 1_2_3.4_5_6e0_0_1");
28+
}
29+
},
30+
{
31+
name: "Decimal bad syntax",
32+
body: function () {
33+
// 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");
36+
assert.throws(()=>eval('1_'), SyntaxError, "Decimal may not end in a numeric separator");
37+
assert.throws(()=>eval('1__'), SyntaxError, "Decimal may not end in a numeric separator");
38+
assert.throws(()=>eval('__1'), ReferenceError, "Decimal may not begin with a numeric separator");
39+
assert.throws(()=>eval('_1'), ReferenceError, "Decimal may not begin with a numeric separator");
40+
41+
// Decimal with right-part with numeric separators
42+
assert.throws(()=>eval('1.0__0'), SyntaxError, "Decimal right-part may not contain multiple contiguous numeric separators");
43+
assert.throws(()=>eval('1.0_0__2'), SyntaxError, "Decimal right-part may not contain multiple contiguous numeric separators");
44+
assert.throws(()=>eval('1._'), SyntaxError, "Decimal right-part may not be a single numeric separator");
45+
assert.throws(()=>eval('1.__'), SyntaxError, "Decimal right-part may not be multiple numeric separators");
46+
assert.throws(()=>eval('1._0'), SyntaxError, "Decimal right-part may not begin with a numeric separator");
47+
assert.throws(()=>eval('1.__0'), SyntaxError, "Decimal right-part may not begin with a numeric separator");
48+
assert.throws(()=>eval('1.0_'), SyntaxError, "Decimal right-part may not end with a numeric separator");
49+
assert.throws(()=>eval('1.0__'), SyntaxError, "Decimal right-part may not end with a numeric separator");
50+
51+
// Decimal with both parts with numeric separators
52+
assert.throws(()=>eval('1_.0'), SyntaxError, "Decimal left-part may not end in numeric separator");
53+
assert.throws(()=>eval('1__.0'), SyntaxError, "Decimal left-part may not end in numeric separator");
54+
assert.throws(()=>eval('1__2.0'), SyntaxError, "Decimal left-part may not contain multiple contiguous numeric separators");
55+
56+
// Decimal with exponent with numeric separators
57+
assert.throws(()=>eval('1_e10'), SyntaxError, "Decimal left-part may not end in numeric separator");
58+
assert.throws(()=>eval('1e_1'), SyntaxError, "Exponent may not begin with numeric separator");
59+
assert.throws(()=>eval('1e__1'), SyntaxError, "Exponent may not begin with numeric separator");
60+
assert.throws(()=>eval('1e1_'), SyntaxError, "Exponent may not end with numeric separator");
61+
assert.throws(()=>eval('1e1__'), SyntaxError, "Exponent may not end with numeric separator");
62+
assert.throws(()=>eval('1e1__2'), SyntaxError, "Exponent may not contain multiple contiguous numeric separators");
63+
64+
// Decimal big ints with numeric separators
65+
assert.throws(()=>eval('1_n'), SyntaxError);
66+
}
67+
},
68+
];
69+
70+
testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });

test/Number/rlexe.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,10 @@
6363
<compile-flags>-args summary -endargs</compile-flags>
6464
</default>
6565
</test>
66+
<test>
67+
<default>
68+
<files>NumericSeparator.js</files>
69+
<compile-flags>-ESNumericSeparator -args summary -endargs</compile-flags>
70+
</default>
71+
</test>
6672
</regress-exe>

0 commit comments

Comments
 (0)