Bug 1421400 - Part 3: Implement "Numeric Separator" stage 3 proposal. r=jorendorff
authorAndré Bargull <andre.bargull@gmail.com>
Sun, 05 May 2019 09:56:09 +0000
changeset 531508 e2a79742eaeaf19010aa96492b401af71b4d0581
parent 531507 11d3fdc18c479945ef0f229cd2a6ce0fd83aa64a
child 531509 c73ca3e739097205c283f3bc7b3b0b771ff7c497
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs1421400
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1421400 - Part 3: Implement "Numeric Separator" stage 3 proposal. r=jorendorff TokenStream.cpp: - Allow '_' between digits per the proposal. - It's a SyntaxError if '_' is not followed by another digit. - It's also a SyntaxError if '_' is not preceded by a digit, e.g. |1._1|. - '_' is not allowed in legacy octal literals and (the integer part of) noctal literals (|07_1| and |09_1| are disallowed, but |09.1_2| is allowed). jsnum.{h,cpp}: - Add an option to GetPrefixInteger(...) to ignore '_' in numbers when called from TokenStream. - Add GetDecimalNonInteger(...) as the counterpart of GetDecimalInteger(...) which parses non-integer decimals. Adding a new function is simpler than modifying js_strtod to conditionally ignore '_' in this case. - GetDecimalInteger(...) and GetDecimalNonInteger(...) are only called from TokenStream, so both functions always ignore '_'. - ComputeAccurateDecimalInteger(...) and ComputeAccurateBinaryBaseInteger(...) both now ignore '_'. This is correct even when called from GetPrefixInteger(...) with |IntegerSeparatorHandling::None|, because GetPrefixInteger(...) already selects the parseable integer prefix. Differential Revision: https://phabricator.services.mozilla.com/D28531
js/src/frontend/TokenStream.cpp
js/src/js.msg
js/src/jsnum.cpp
js/src/jsnum.h
js/src/vm/JSONParser.cpp
--- a/js/src/frontend/TokenStream.cpp
+++ b/js/src/frontend/TokenStream.cpp
@@ -2176,17 +2176,28 @@ MOZ_MUST_USE MOZ_ALWAYS_INLINE bool
 TokenStreamSpecific<Unit, AnyCharsAccess>::matchInteger(
     IsIntegerUnit isIntegerUnit, int32_t* nextUnit) {
   int32_t unit;
   while (true) {
     unit = getCodeUnit();
     if (isIntegerUnit(unit)) {
       continue;
     }
+#ifdef NIGHTLY_BUILD
+    if (unit != '_') {
+      break;
+    }
+    unit = getCodeUnit();
+    if (!isIntegerUnit(unit)) {
+      error(JSMSG_MISSING_DIGIT_AFTER_SEPARATOR);
+      return false;
+    }
+#else
     break;
+#endif /* NIGHTLY_BUILD */
   }
 
   *nextUnit = unit;
   return true;
 }
 
 template <typename Unit, class AnyCharsAccess>
 MOZ_MUST_USE bool TokenStreamSpecific<Unit, AnyCharsAccess>::decimalNumber(
@@ -2249,18 +2260,19 @@ MOZ_MUST_USE bool TokenStreamSpecific<Un
       }
     }
 
     ungetCodeUnit(unit);
 
     // "0." and "0e..." numbers parse "." or "e..." here.  Neither range
     // contains a number, so we can't use |FullStringToDouble|.  (Parse
     // failures return 0.0, so we'll still get the right result.)
-    if (!StringToDouble(anyCharsAccess().cx, numStart,
-                        this->sourceUnits.addressOfNextCodeUnit(), &dval)) {
+    if (!GetDecimalNonInteger(anyCharsAccess().cx, numStart,
+                              this->sourceUnits.addressOfNextCodeUnit(),
+                              &dval)) {
       return false;
     }
   }
 
   // Number followed by IdentifierStart is an error.  (This is the only place
   // in ECMAScript where token boundary is inadequate to properly separate
   // two tokens, necessitating this unaesthetic lookahead.)
   if (unit != EOF) {
@@ -2427,16 +2439,22 @@ MOZ_MUST_USE bool TokenStreamSpecific<Un
   mozilla::Range<const Unit> chars(
       this->sourceUnits.codeUnitPtrAt(start.offset()), length);
   for (uint32_t idx = 0; idx < length - 1; idx++) {
     int32_t unit = CodeUnitValue(chars[idx]);
     // Char buffer may start with a 0[bBoOxX] prefix, then follows with
     // binary, octal, decimal, or hex digits.  Already checked by caller, as
     // the "n" indicating bigint comes at the end.
     MOZ_ASSERT(isAsciiCodePoint(unit));
+#ifdef NIGHTLY_BUILD
+    // Skip over any separators.
+    if (unit == '_') {
+      continue;
+    }
+#endif
     if (!this->appendCodePointToCharBuffer(unit)) {
       return false;
     }
   }
   newBigIntToken(start, modifier, out);
   return true;
 }
 
@@ -2692,22 +2710,35 @@ MOZ_MUST_USE bool TokenStreamSpecific<Un
         bool nonOctalDecimalIntegerLiteral = false;
         do {
           if (unit >= '8') {
             nonOctalDecimalIntegerLiteral = true;
           }
           unit = getCodeUnit();
         } while (IsAsciiDigit(unit));
 
+#ifdef NIGHTLY_BUILD
+        if (unit == '_') {
+          error(JSMSG_SEPARATOR_IN_ZERO_PREFIXED_NUMBER);
+          return badToken();
+        }
+#endif /* NIGHTLY_BUILD */
+
         if (nonOctalDecimalIntegerLiteral) {
           // Use the decimal scanner for the rest of the number.
           return decimalNumber(unit, start, numStart, modifier, ttp);
         }
+#ifdef NIGHTLY_BUILD
+      } else if (unit == '_') {
+        // Give a more explicit error message when '_' is used after '0'.
+        error(JSMSG_SEPARATOR_IN_ZERO_PREFIXED_NUMBER);
+        return badToken();
+#endif /* NIGHTLY_BUILD */
       } else {
-        // '0' not followed by [XxBbOo0-9];  scan as a decimal number.
+        // '0' not followed by [XxBbOo0-9_];  scan as a decimal number.
         numStart = this->sourceUnits.addressOfNextCodeUnit() - 1;
 
         // NOTE: |unit| may be EOF here.  (This is permitted by case #3
         //       in TokenStream.h docs for this function.)
         return decimalNumber(unit, start, numStart, modifier, ttp);
       }
 
       if (unit == 'n' && anyCharsAccess().options().bigIntEnabledOption) {
@@ -2742,17 +2773,17 @@ MOZ_MUST_USE bool TokenStreamSpecific<Un
 
       if (isBigInt) {
         return bigIntLiteral(start, modifier, ttp);
       }
 
       double dval;
       if (!GetFullInteger(anyCharsAccess().cx, numStart,
                           this->sourceUnits.addressOfNextCodeUnit(), radix,
-                          &dval)) {
+                          IntegerSeparatorHandling::SkipUnderscore, &dval)) {
         return badToken();
       }
       newNumberToken(dval, NoDecimal, start, modifier, ttp);
       return true;
     }
 
     MOZ_ASSERT(c1kind == Other);
 
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -265,31 +265,33 @@ MSG_DEF(JSMSG_BAD_ESCAPE,              0
 MSG_DEF(JSMSG_MISSING_PRIVATE_NAME,    0, JSEXN_SYNTAXERR, "'#' not followed by identifier")
 MSG_DEF(JSMSG_ILLEGAL_CHARACTER,       0, JSEXN_SYNTAXERR, "illegal character")
 MSG_DEF(JSMSG_IMPORT_META_OUTSIDE_MODULE, 0, JSEXN_SYNTAXERR, "import.meta may only appear in a module")
 MSG_DEF(JSMSG_IMPORT_DECL_AT_TOP_LEVEL, 0, JSEXN_SYNTAXERR, "import declarations may only appear at top level of a module")
 MSG_DEF(JSMSG_OF_AFTER_FOR_LOOP_DECL,  0, JSEXN_SYNTAXERR, "a declaration in the head of a for-of loop can't have an initializer")
 MSG_DEF(JSMSG_IN_AFTER_LEXICAL_FOR_DECL,0,JSEXN_SYNTAXERR, "a lexical declaration in the head of a for-in loop can't have an initializer")
 MSG_DEF(JSMSG_INVALID_FOR_IN_DECL_WITH_INIT,0,JSEXN_SYNTAXERR,"for-in loop head declarations may not have initializers")
 MSG_DEF(JSMSG_INVALID_ID,              1, JSEXN_SYNTAXERR, "{0} is an invalid identifier")
+MSG_DEF(JSMSG_SEPARATOR_IN_ZERO_PREFIXED_NUMBER, 0, JSEXN_SYNTAXERR, "numeric separators '_' are not allowed in numbers that start with '0'")
 MSG_DEF(JSMSG_LABEL_NOT_FOUND,         0, JSEXN_SYNTAXERR, "label not found")
 MSG_DEF(JSMSG_LEXICAL_DECL_NOT_IN_BLOCK,   1, JSEXN_SYNTAXERR, "{0} declaration not directly within block")
 MSG_DEF(JSMSG_LEXICAL_DECL_LABEL,      1, JSEXN_SYNTAXERR, "{0} declarations cannot be labelled")
 MSG_DEF(JSMSG_GENERATOR_LABEL,         0, JSEXN_SYNTAXERR, "generator functions cannot be labelled")
 MSG_DEF(JSMSG_FUNCTION_LABEL,          0, JSEXN_SYNTAXERR, "functions cannot be labelled")
 MSG_DEF(JSMSG_SLOPPY_FUNCTION_LABEL,   0, JSEXN_SYNTAXERR, "functions can only be labelled inside blocks")
 MSG_DEF(JSMSG_LINE_BREAK_AFTER_THROW,  0, JSEXN_SYNTAXERR, "no line break is allowed between 'throw' and its expression")
 MSG_DEF(JSMSG_LINE_BREAK_BEFORE_ARROW, 0, JSEXN_SYNTAXERR, "no line break is allowed before '=>'")
 MSG_DEF(JSMSG_MALFORMED_ESCAPE,        1, JSEXN_SYNTAXERR, "malformed {0} character escape sequence")
 MSG_DEF(JSMSG_MISSING_BINARY_DIGITS,   0, JSEXN_SYNTAXERR, "missing binary digits after '0b'")
 MSG_DEF(JSMSG_MISSING_EXPONENT,        0, JSEXN_SYNTAXERR, "missing exponent")
 MSG_DEF(JSMSG_MISSING_EXPR_AFTER_THROW,0, JSEXN_SYNTAXERR, "throw statement is missing an expression")
 MSG_DEF(JSMSG_MISSING_FORMAL,          0, JSEXN_SYNTAXERR, "missing formal parameter")
 MSG_DEF(JSMSG_MISSING_HEXDIGITS,       0, JSEXN_SYNTAXERR, "missing hexadecimal digits after '0x'")
 MSG_DEF(JSMSG_MISSING_OCTAL_DIGITS,    0, JSEXN_SYNTAXERR, "missing octal digits after '0o'")
+MSG_DEF(JSMSG_MISSING_DIGIT_AFTER_SEPARATOR, 0, JSEXN_SYNTAXERR, "missing digit after '_' numeric separator")
 MSG_DEF(JSMSG_MODULE_SPEC_AFTER_FROM,  0, JSEXN_SYNTAXERR, "missing module specifier after 'from' keyword")
 MSG_DEF(JSMSG_NAME_AFTER_DOT,          0, JSEXN_SYNTAXERR, "missing name after . operator")
 MSG_DEF(JSMSG_NAMED_IMPORTS_OR_NAMESPACE_IMPORT, 0, JSEXN_SYNTAXERR, "expected named imports or namespace import after comma")
 MSG_DEF(JSMSG_NO_BINDING_NAME,        0, JSEXN_SYNTAXERR, "missing binding name")
 MSG_DEF(JSMSG_NO_EXPORT_NAME,          0, JSEXN_SYNTAXERR, "missing export name")
 MSG_DEF(JSMSG_NO_IMPORT_NAME,          0, JSEXN_SYNTAXERR, "missing import name")
 MSG_DEF(JSMSG_NO_VARIABLE_NAME,        0, JSEXN_SYNTAXERR, "missing variable name")
 MSG_DEF(JSMSG_OF_AFTER_FOR_NAME,       0, JSEXN_SYNTAXERR, "missing 'of' after for")
--- a/js/src/jsnum.cpp
+++ b/js/src/jsnum.cpp
@@ -76,36 +76,55 @@ static bool EnsureDtoaState(JSContext* c
     cx->dtoaState = NewDtoaState();
     if (!cx->dtoaState) {
       return false;
     }
   }
   return true;
 }
 
+template <typename CharT>
+static inline void AssertWellPlacedNumericSeparator(const CharT* s,
+                                                    const CharT* start,
+                                                    const CharT* end) {
+  MOZ_ASSERT(start < end, "string is non-empty");
+  MOZ_ASSERT(s > start, "number can't start with a separator");
+  MOZ_ASSERT(s + 1 < end,
+             "final character in a numeric literal can't be a separator");
+  MOZ_ASSERT(*(s + 1) != '_',
+             "separator can't be followed by another separator");
+  MOZ_ASSERT(*(s - 1) != '_',
+             "separator can't be preceded by another separator");
+}
+
 /*
  * If we're accumulating a decimal number and the number is >= 2^53, then the
  * fast result from the loop in Get{Prefix,Decimal}Integer may be inaccurate.
  * Call js_strtod_harder to get the correct answer.
  */
 template <typename CharT>
 static bool ComputeAccurateDecimalInteger(JSContext* cx, const CharT* start,
                                           const CharT* end, double* dp) {
   size_t length = end - start;
   auto cstr = cx->make_pod_array<char>(length + 1);
   if (!cstr) {
     return false;
   }
 
+  size_t j = 0;
   for (size_t i = 0; i < length; i++) {
     char c = char(start[i]);
+    if (c == '_') {
+      AssertWellPlacedNumericSeparator(start + i, start, end);
+      continue;
+    }
     MOZ_ASSERT(IsAsciiAlphanumeric(c));
-    cstr[i] = c;
+    cstr[j++] = c;
   }
-  cstr[length] = 0;
+  cstr[j] = 0;
 
   if (!EnsureDtoaState(cx)) {
     return false;
   }
 
   char* estr;
   *dp = js_strtod_harder(cx->dtoaState, cstr.get(), &estr);
 
@@ -114,31 +133,42 @@ static bool ComputeAccurateDecimalIntege
 
 namespace {
 
 template <typename CharT>
 class BinaryDigitReader {
   const int base;     /* Base of number; must be a power of 2 */
   int digit;          /* Current digit value in radix given by base */
   int digitMask;      /* Mask to extract the next bit from digit */
-  const CharT* start; /* Pointer to the remaining digits */
+  const CharT* cur;   /* Pointer to the remaining digits */
+  const CharT* start; /* Pointer to the start of the string */
   const CharT* end;   /* Pointer to first non-digit */
 
  public:
   BinaryDigitReader(int base, const CharT* start, const CharT* end)
-      : base(base), digit(0), digitMask(0), start(start), end(end) {}
+      : base(base),
+        digit(0),
+        digitMask(0),
+        cur(start),
+        start(start),
+        end(end) {}
 
   /* Return the next binary digit from the number, or -1 if done. */
   int nextDigit() {
     if (digitMask == 0) {
-      if (start == end) {
+      if (cur == end) {
         return -1;
       }
 
-      int c = *start++;
+      int c = *cur++;
+      if (c == '_') {
+        AssertWellPlacedNumericSeparator(cur - 1, start, end);
+        c = *cur++;
+      }
+
       MOZ_ASSERT(IsAsciiAlphanumeric(c));
       digit = AsciiAlphanumericToNumber(c);
       digitMask = base >> 1;
     }
 
     int bit = (digit & digitMask) != 0;
     digitMask >>= 1;
     return bit;
@@ -217,25 +247,31 @@ double js::ParseDecimalNumber(const mozi
 template double js::ParseDecimalNumber(
     const mozilla::Range<const Latin1Char> chars);
 
 template double js::ParseDecimalNumber(
     const mozilla::Range<const char16_t> chars);
 
 template <typename CharT>
 bool js::GetPrefixInteger(JSContext* cx, const CharT* start, const CharT* end,
-                          int base, const CharT** endp, double* dp) {
+                          int base, IntegerSeparatorHandling separatorHandling,
+                          const CharT** endp, double* dp) {
   MOZ_ASSERT(start <= end);
   MOZ_ASSERT(2 <= base && base <= 36);
 
   const CharT* s = start;
   double d = 0.0;
   for (; s < end; s++) {
     CharT c = *s;
     if (!IsAsciiAlphanumeric(c)) {
+      if (c == '_' &&
+          separatorHandling == IntegerSeparatorHandling::SkipUnderscore) {
+        AssertWellPlacedNumericSeparator(s, start, end);
+        continue;
+      }
       break;
     }
 
     uint8_t digit = AsciiAlphanumericToNumber(c);
     if (digit >= base) {
       break;
     }
 
@@ -248,50 +284,56 @@ bool js::GetPrefixInteger(JSContext* cx,
   /* If we haven't reached the limit of integer precision, we're done. */
   if (d < DOUBLE_INTEGRAL_PRECISION_LIMIT) {
     return true;
   }
 
   /*
    * Otherwise compute the correct integer from the prefix of valid digits
    * if we're computing for base ten or a power of two.  Don't worry about
-   * other bases; see 15.1.2.2 step 13.
+   * other bases; see ES2018, 18.2.5 `parseInt(string, radix)`, step 13.
    */
   if (base == 10) {
     return ComputeAccurateDecimalInteger(cx, start, s, dp);
   }
 
   if ((base & (base - 1)) == 0) {
     *dp = ComputeAccurateBinaryBaseInteger(start, s, base);
   }
 
   return true;
 }
 
 namespace js {
 
 template bool GetPrefixInteger(JSContext* cx, const char16_t* start,
                                const char16_t* end, int base,
+                               IntegerSeparatorHandling separatorHandling,
                                const char16_t** endp, double* dp);
 
 template bool GetPrefixInteger(JSContext* cx, const Latin1Char* start,
                                const Latin1Char* end, int base,
+                               IntegerSeparatorHandling separatorHandling,
                                const Latin1Char** endp, double* dp);
 
 }  // namespace js
 
 template <typename CharT>
 bool js::GetDecimalInteger(JSContext* cx, const CharT* start, const CharT* end,
                            double* dp) {
   MOZ_ASSERT(start <= end);
 
   const CharT* s = start;
   double d = 0.0;
   for (; s < end; s++) {
     CharT c = *s;
+    if (c == '_') {
+      AssertWellPlacedNumericSeparator(s, start, end);
+      continue;
+    }
     MOZ_ASSERT(IsAsciiDigit(c));
     int digit = c - '0';
     d = d * 10 + digit;
   }
 
   *dp = d;
 
   // If we haven't reached the limit of integer precision, we're done.
@@ -315,16 +357,69 @@ template <>
 bool GetDecimalInteger<Utf8Unit>(JSContext* cx, const Utf8Unit* start,
                                  const Utf8Unit* end, double* dp) {
   return GetDecimalInteger(cx, Utf8AsUnsignedChars(start),
                            Utf8AsUnsignedChars(end), dp);
 }
 
 }  // namespace js
 
+template <typename CharT>
+bool js::GetDecimalNonInteger(JSContext* cx, const CharT* start,
+                              const CharT* end, double* dp) {
+  MOZ_ASSERT(start <= end);
+
+  size_t length = end - start;
+  Vector<char, 32> chars(cx);
+  if (!chars.growByUninitialized(length + 1)) {
+    return false;
+  }
+
+  const CharT* s = start;
+  size_t i = 0;
+  for (; s < end; s++) {
+    CharT c = *s;
+    if (c == '_') {
+      AssertWellPlacedNumericSeparator(s, start, end);
+      continue;
+    }
+    MOZ_ASSERT(IsAsciiDigit(c) || c == '.' || c == 'e' || c == 'E' ||
+               c == '+' || c == '-');
+    chars[i++] = char(c);
+  }
+  chars[i] = 0;
+
+  if (!EnsureDtoaState(cx)) {
+    return false;
+  }
+
+  char* ep;
+  *dp = js_strtod_harder(cx->dtoaState, chars.begin(), &ep);
+  MOZ_ASSERT(ep >= chars.begin());
+
+  return true;
+}
+
+namespace js {
+
+template bool GetDecimalNonInteger(JSContext* cx, const char16_t* start,
+                                   const char16_t* end, double* dp);
+
+template bool GetDecimalNonInteger(JSContext* cx, const Latin1Char* start,
+                                   const Latin1Char* end, double* dp);
+
+template <>
+bool GetDecimalNonInteger<Utf8Unit>(JSContext* cx, const Utf8Unit* start,
+                                    const Utf8Unit* end, double* dp) {
+  return GetDecimalNonInteger(cx, Utf8AsUnsignedChars(start),
+                              Utf8AsUnsignedChars(end), dp);
+}
+
+}  // namespace js
+
 static bool num_parseFloat(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   if (args.length() == 0) {
     args.rval().setNaN();
     return true;
   }
 
@@ -403,17 +498,18 @@ static bool ParseIntImpl(JSContext* cx, 
       s += 2;
       radix = 16;
     }
   }
 
   /* Steps 11-15. */
   const CharT* actualEnd;
   double d;
-  if (!GetPrefixInteger(cx, s, end, radix, &actualEnd, &d)) {
+  if (!GetPrefixInteger(cx, s, end, radix, IntegerSeparatorHandling::None,
+                        &actualEnd, &d)) {
     return false;
   }
 
   if (s == actualEnd) {
     *res = GenericNaN();
   } else {
     *res = negative ? -d : d;
   }
@@ -1573,17 +1669,18 @@ static bool CharsToNumber(JSContext* cx,
     if (radix != 0) {
       /*
        * It's probably a non-decimal number. Accept if there's at least one
        * digit after the 0b|0o|0x, and if no non-whitespace characters follow
        * all the digits.
        */
       const CharT* endptr;
       double d;
-      if (!GetPrefixInteger(cx, bp + 2, end, radix, &endptr, &d) ||
+      if (!GetPrefixInteger(cx, bp + 2, end, radix,
+                            IntegerSeparatorHandling::None, &endptr, &d) ||
           endptr == bp + 2 || SkipSpace(endptr, end) != end) {
         *result = GenericNaN();
       } else {
         *result = d;
       }
       return true;
     }
   }
--- a/js/src/jsnum.h
+++ b/js/src/jsnum.h
@@ -116,69 +116,86 @@ const double DOUBLE_INTEGRAL_PRECISION_L
  * Parse a decimal number encoded in |chars|.  The decimal number must be
  * sufficiently small that it will not overflow the integrally-precise range of
  * the double type -- that is, the number will be smaller than
  * DOUBLE_INTEGRAL_PRECISION_LIMIT
  */
 template <typename CharT>
 extern double ParseDecimalNumber(const mozilla::Range<const CharT> chars);
 
+enum class IntegerSeparatorHandling : bool { None, SkipUnderscore };
+
 /*
  * Compute the positive integer of the given base described immediately at the
  * start of the range [start, end) -- no whitespace-skipping, no magical
  * leading-"0" octal or leading-"0x" hex behavior, no "+"/"-" parsing, just
  * reading the digits of the integer.  Return the index one past the end of the
  * digits of the integer in *endp, and return the integer itself in *dp.  If
  * base is 10 or a power of two the returned integer is the closest possible
  * double; otherwise extremely large integers may be slightly inaccurate.
  *
+ * The |separatorHandling| controls whether or not numeric separators can be
+ * part of integer string. If the option is enabled, all '_' characters in the
+ * string are ignored. Underscore characters must not appear directly next to
+ * each other, e.g. '1__2' will lead to an assertion.
+ *
  * If [start, end) does not begin with a number with the specified base,
  * *dp == 0 and *endp == start upon return.
  */
 template <typename CharT>
-extern MOZ_MUST_USE bool GetPrefixInteger(JSContext* cx, const CharT* start,
-                                          const CharT* end, int base,
-                                          const CharT** endp, double* dp);
+extern MOZ_MUST_USE bool GetPrefixInteger(
+    JSContext* cx, const CharT* start, const CharT* end, int base,
+    IntegerSeparatorHandling separatorHandling, const CharT** endp, double* dp);
 
 inline const char16_t* ToRawChars(const char16_t* units) { return units; }
 
 inline const unsigned char* ToRawChars(const unsigned char* units) {
   return units;
 }
 
 inline const unsigned char* ToRawChars(const mozilla::Utf8Unit* units) {
   return mozilla::Utf8AsUnsignedChars(units);
 }
 
 /**
- * Like the prior function, but [start, end) must all be digits in the given
+ * Like GetPrefixInteger, but [start, end) must all be digits in the given
  * base (and so this function doesn't take a useless outparam).
  */
 template <typename CharT>
-extern MOZ_MUST_USE bool GetFullInteger(JSContext* cx, const CharT* start,
-                                        const CharT* end, int base,
-                                        double* dp) {
+extern MOZ_MUST_USE bool GetFullInteger(
+    JSContext* cx, const CharT* start, const CharT* end, int base,
+    IntegerSeparatorHandling separatorHandling, double* dp) {
   decltype(ToRawChars(start)) realEnd;
-  if (GetPrefixInteger(cx, ToRawChars(start), ToRawChars(end), base, &realEnd,
-                       dp)) {
+  if (GetPrefixInteger(cx, ToRawChars(start), ToRawChars(end), base,
+                       separatorHandling, &realEnd, dp)) {
     MOZ_ASSERT(end == static_cast<const void*>(realEnd));
     return true;
   }
   return false;
 }
 
 /*
- * This is like GetPrefixInteger, but it only deals with base 10 and doesn't
- * have an |endp| outparam.  It should only be used when the characters are
- * known to only contain digits.
+ * This is like GetPrefixInteger, but only deals with base 10, always ignores
+ * '_', and doesn't have an |endp| outparam. It should only be used when the
+ * characters are known to match |DecimalIntegerLiteral|, cf. ES2020, 11.8.3
+ * Numeric Literals.
  */
 template <typename CharT>
 extern MOZ_MUST_USE bool GetDecimalInteger(JSContext* cx, const CharT* start,
                                            const CharT* end, double* dp);
 
+/*
+ * This is like GetDecimalInteger, but also allows non-integer numbers. It
+ * should only be used when the characters are known to match |DecimalLiteral|,
+ * cf. ES2020, 11.8.3 Numeric Literals.
+ */
+template <typename CharT>
+extern MOZ_MUST_USE bool GetDecimalNonInteger(JSContext* cx, const CharT* start,
+                                              const CharT* end, double* dp);
+
 extern MOZ_MUST_USE bool StringToNumber(JSContext* cx, JSString* str,
                                         double* result);
 
 extern MOZ_MUST_USE bool StringToNumberPure(JSContext* cx, JSString* str,
                                             double* result);
 
 /* ES5 9.3 ToNumber, overwriting *vp with the appropriate number value. */
 MOZ_ALWAYS_INLINE MOZ_MUST_USE bool ToNumber(JSContext* cx,
@@ -240,28 +257,16 @@ MOZ_MUST_USE bool num_parseInt(JSContext
 template <typename CharT>
 extern MOZ_MUST_USE bool js_strtod(JSContext* cx, const CharT* begin,
                                    const CharT* end, const CharT** dEnd,
                                    double* d);
 
 namespace js {
 
 /**
- * Like js_strtod, but for when you don't require a |dEnd| argument *and* it's
- * possible that the number in the string will not occupy the full [begin, end)
- * range.
- */
-template <typename CharT>
-extern MOZ_MUST_USE bool StringToDouble(JSContext* cx, const CharT* begin,
-                                        const CharT* end, double* d) {
-  decltype(ToRawChars(begin)) dummy;
-  return js_strtod(cx, ToRawChars(begin), ToRawChars(end), &dummy, d);
-}
-
-/**
  * Like js_strtod, but for when the number always constitutes the entire range
  * (and so |dEnd| would be a value already known).
  */
 template <typename CharT>
 extern MOZ_MUST_USE bool FullStringToDouble(JSContext* cx, const CharT* begin,
                                             const CharT* end, double* d) {
   decltype(ToRawChars(begin)) realEnd;
   if (js_strtod(cx, ToRawChars(begin), ToRawChars(end), &realEnd, d)) {
--- a/js/src/vm/JSONParser.cpp
+++ b/js/src/vm/JSONParser.cpp
@@ -292,17 +292,18 @@ JSONParserBase::Token JSONParser<CharT>:
       // largest number a double can represent with integral precision),
       // parse it using a decimal-only parser.  This comparison is
       // conservative but faster than a fully-precise check.
       double d = ParseDecimalNumber(chars);
       return numberToken(negative ? -d : d);
     }
 
     double d;
-    if (!GetFullInteger(cx, digitStart.get(), current.get(), 10, &d)) {
+    if (!GetFullInteger(cx, digitStart.get(), current.get(), 10,
+                        IntegerSeparatorHandling::None, &d)) {
       return token(OOM);
     }
     return numberToken(negative ? -d : d);
   }
 
   /* (\.[0-9]+)? */
   if (current < end && *current == '.') {
     if (++current == end) {