lib/VM/JSLib/Number.cpp (549 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
//===----------------------------------------------------------------------===//
/// \file
/// ES5.1 15.7 Initialize the Number constructor.
//===----------------------------------------------------------------------===//
#include "JSLibInternal.h"
#include "hermes/VM/Operations.h"
#include "hermes/VM/PrimitiveBox.h"
#include "hermes/VM/SmallXString.h"
#include "hermes/VM/StringPrimitive.h"
#include "dtoa/dtoa.h"
#include "llvh/ADT/SmallString.h"
#include "llvh/Support/Format.h"
namespace hermes {
namespace vm {
Handle<JSObject> createNumberConstructor(Runtime &runtime) {
auto numberPrototype = Handle<JSNumber>::vmcast(&runtime.numberPrototype);
auto cons = defineSystemConstructor<JSNumber>(
runtime,
Predefined::getSymbolID(Predefined::Number),
numberConstructor,
numberPrototype,
1,
CellKind::JSNumberKind);
defineMethod(
runtime,
numberPrototype,
Predefined::getSymbolID(Predefined::valueOf),
nullptr,
numberPrototypeValueOf,
0);
defineMethod(
runtime,
numberPrototype,
Predefined::getSymbolID(Predefined::toString),
nullptr,
numberPrototypeToString,
1);
defineMethod(
runtime,
numberPrototype,
Predefined::getSymbolID(Predefined::toLocaleString),
nullptr,
numberPrototypeToLocaleString,
0);
defineMethod(
runtime,
numberPrototype,
Predefined::getSymbolID(Predefined::toFixed),
nullptr,
numberPrototypeToFixed,
1);
defineMethod(
runtime,
numberPrototype,
Predefined::getSymbolID(Predefined::toExponential),
nullptr,
numberPrototypeToExponential,
1);
defineMethod(
runtime,
numberPrototype,
Predefined::getSymbolID(Predefined::toPrecision),
nullptr,
numberPrototypeToPrecision,
1);
DefinePropertyFlags constantDPF =
DefinePropertyFlags::getDefaultNewPropertyFlags();
constantDPF.enumerable = 0;
constantDPF.writable = 0;
constantDPF.configurable = 0;
MutableHandle<> numberValueHandle{runtime};
auto setNumberValueProperty = [&](SymbolID name, double value) {
numberValueHandle = HermesValue::encodeDoubleValue(value);
auto result = JSObject::defineOwnProperty(
cons, runtime, name, constantDPF, numberValueHandle);
assert(
result != ExecutionStatus::EXCEPTION &&
"defineOwnProperty() failed on a new object");
(void)result;
};
setNumberValueProperty(
Predefined::getSymbolID(Predefined::MAX_VALUE),
std::numeric_limits<double>::max());
setNumberValueProperty(
Predefined::getSymbolID(Predefined::MIN_VALUE),
std::numeric_limits<double>::denorm_min());
setNumberValueProperty(
Predefined::getSymbolID(Predefined::NaN),
std::numeric_limits<double>::quiet_NaN());
setNumberValueProperty(
Predefined::getSymbolID(Predefined::NEGATIVE_INFINITY),
-std::numeric_limits<double>::infinity());
setNumberValueProperty(
Predefined::getSymbolID(Predefined::POSITIVE_INFINITY),
std::numeric_limits<double>::infinity());
// ES6.0 20.1.2.1
setNumberValueProperty(
Predefined::getSymbolID(Predefined::EPSILON),
std::numeric_limits<double>::epsilon());
// ES6.0 20.1.2.6
constexpr double kMaxSafeInteger = 9007199254740991;
setNumberValueProperty(
Predefined::getSymbolID(Predefined::MAX_SAFE_INTEGER), kMaxSafeInteger);
// ES6.0 20.1.2.8
setNumberValueProperty(
Predefined::getSymbolID(Predefined::MIN_SAFE_INTEGER), -kMaxSafeInteger);
defineMethod(
runtime,
cons,
Predefined::getSymbolID(Predefined::isFinite),
nullptr,
numberIsFinite,
1);
defineMethod(
runtime,
cons,
Predefined::getSymbolID(Predefined::isInteger),
nullptr,
numberIsInteger,
1);
defineMethod(
runtime,
cons,
Predefined::getSymbolID(Predefined::isNaN),
nullptr,
numberIsNaN,
1);
defineMethod(
runtime,
cons,
Predefined::getSymbolID(Predefined::isSafeInteger),
nullptr,
numberIsSafeInteger,
1);
defineProperty(
runtime,
cons,
Predefined::getSymbolID(Predefined::parseInt),
Handle<>(&runtime.parseIntFunction));
defineProperty(
runtime,
cons,
Predefined::getSymbolID(Predefined::parseFloat),
Handle<>(&runtime.parseFloatFunction));
return cons;
}
CallResult<HermesValue>
numberConstructor(void *, Runtime &runtime, NativeArgs args) {
double value = +0.0;
if (args.getArgCount() > 0) {
auto res = toNumber_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
value = res->getNumber();
}
if (args.isConstructorCall()) {
auto *self = vmcast<JSNumber>(args.getThisArg());
self->setPrimitiveNumber(value);
return args.getThisArg();
}
return HermesValue::encodeDoubleValue(value);
}
CallResult<HermesValue>
numberIsFinite(void *, Runtime &runtime, NativeArgs args) {
if (!args.getArg(0).isNumber()) {
// If Type(number) is not Number, return false.
return HermesValue::encodeBoolValue(false);
}
double number = args.getArg(0).getNumber();
// If number is NaN, +∞, or −∞, return false.
// Otherwise, return true.
return HermesValue::encodeBoolValue(std::isfinite(number));
}
CallResult<HermesValue>
numberIsInteger(void *, Runtime &runtime, NativeArgs args) {
if (!args.getArg(0).isNumber()) {
// If Type(number) is not Number, return false.
return HermesValue::encodeBoolValue(false);
}
double number = args.getArg(0).getNumber();
if (!std::isfinite(number)) {
// If number is NaN, +∞, or −∞, return false.
return HermesValue::encodeBoolValue(false);
}
// Let integer be ToIntegerOrInfinity(number).
assert(!std::isnan(number) && "number must not be NaN after the check");
// Call std::trunc because we've alredy checked NaN with isfinite.
double integer = std::trunc(number);
// If integer is not equal to number, return false.
// Otherwise, return true.
return HermesValue::encodeBoolValue(integer == number);
}
CallResult<HermesValue> numberIsNaN(void *, Runtime &runtime, NativeArgs args) {
if (!args.getArg(0).isNumber()) {
// If Type(number) is not Number, return false.
return HermesValue::encodeBoolValue(false);
}
double number = args.getArg(0).getNumber();
// If number is NaN, return true.
// Otherwise, return false.
return HermesValue::encodeBoolValue(std::isnan(number));
}
CallResult<HermesValue>
numberIsSafeInteger(void *, Runtime &runtime, NativeArgs args) {
if (!args.getArg(0).isNumber()) {
// If Type(number) is not Number, return false.
return HermesValue::encodeBoolValue(false);
}
double number = args.getArg(0).getNumber();
if (!std::isfinite(number)) {
// If number is NaN, +∞, or −∞, return false.
return HermesValue::encodeBoolValue(false);
}
// Let integer be ToIntegerOrInfinity(number).
assert(!std::isnan(number) && "number must not be NaN after the check");
// Call std::trunc because we've alredy checked NaN with isfinite.
double integer = std::trunc(number);
if (integer != number) {
// If integer is not equal to number, return false.
return HermesValue::encodeBoolValue(false);
}
// If abs(integer) ≤ 2^53−1, return true.
// Otherwise, return false.
return HermesValue::encodeBoolValue(
std::abs(integer) <= ((double)((uint64_t)1 << 53)) - 1);
}
CallResult<HermesValue>
numberPrototypeValueOf(void *, Runtime &runtime, NativeArgs args) {
if (args.getThisArg().isNumber()) {
return args.getThisArg();
}
auto *numPtr = dyn_vmcast<JSNumber>(args.getThisArg());
if (!numPtr) {
return runtime.raiseTypeError(
"Number.prototype.valueOf() can only be used on Number");
}
return HermesValue::encodeNumberValue(numPtr->getPrimitiveNumber());
}
CallResult<HermesValue>
numberPrototypeToString(void *, Runtime &runtime, NativeArgs args) {
const size_t MIN_RADIX = 2;
const size_t MAX_RADIX = 36;
unsigned radix;
double number;
// Extract the number from this.
if (args.getThisArg().isNumber()) {
number = args.getThisArg().getNumber();
} else {
auto *numPtr = dyn_vmcast<JSNumber>(args.getThisArg());
if (!numPtr) {
return runtime.raiseTypeError(
"Number.prototype.toString() can only be used on Number");
}
number = numPtr->getPrimitiveNumber();
}
if (args.getArg(0).isUndefined())
radix = 10;
else {
// ToIntegerOrInfinity(arg0).
auto intRes = toIntegerOrInfinity(runtime, args.getArgHandle(0));
if (intRes == ExecutionStatus::EXCEPTION) {
return ExecutionStatus::EXCEPTION;
}
auto d = intRes->getNumber();
if (d < MIN_RADIX || d > MAX_RADIX) {
return runtime.raiseRangeError("Invalid radix value");
}
radix = (unsigned)d;
}
// Directly output finite numbers in a non-decimal base.
// Note that this is implementation defined behavior: we output a rounded
// string such that we're as close as possible to the actual precision encoded
// in the double value given by number.
if (radix != 10 && std::isfinite(number)) {
return numberToStringWithRadix(runtime, number, radix).getHermesValue();
}
// Radix 10 and non-finite values simply call toString.
auto resultRes = toString_RJS(
runtime, runtime.makeHandle(HermesValue::encodeNumberValue(number)));
if (LLVM_UNLIKELY(resultRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
return resultRes->getHermesValue();
}
CallResult<HermesValue>
numberPrototypeToLocaleString(void *ctx, Runtime &runtime, NativeArgs args) {
#ifdef HERMES_ENABLE_INTL
return intlNumberPrototypeToLocaleString(/* unused */ ctx, runtime, args);
#else
double number;
// Extract the number from this.
if (args.getThisArg().isNumber()) {
number = args.getThisArg().getNumber();
} else {
auto *numPtr = dyn_vmcast<JSNumber>(args.getThisArg());
if (!numPtr) {
return runtime.raiseTypeError(
"Number.prototype.toLocaleString() can only be used on Number");
}
number = numPtr->getPrimitiveNumber();
}
// Call toString, as JSC does.
// TODO: Format string according to locale.
auto res = toString_RJS(
runtime, runtime.makeHandle(HermesValue::encodeNumberValue(number)));
if (res == ExecutionStatus::EXCEPTION) {
return ExecutionStatus::EXCEPTION;
}
return res->getHermesValue();
#endif
}
CallResult<HermesValue>
numberPrototypeToFixed(void *, Runtime &runtime, NativeArgs args) {
auto intRes = toIntegerOrInfinity(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(intRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double fDouble = intRes->getNumber();
// 3. If f < 0 or f > 100, throw a RangeError exception.
if (LLVM_UNLIKELY(fDouble < 0 || fDouble > 100)) {
return runtime.raiseRangeError(
"toFixed argument must be between 0 and 100");
}
/// Number of digits after the decimal point.
/// Because we checked, 0 <= f <= 20.
/// In particular, we know that f is non-negative.
int32_t f = static_cast<int32_t>(fDouble);
// The number to make a string toFixed.
double x;
if (args.getThisArg().isNumber()) {
x = args.getThisArg().getNumber();
} else {
auto numPtr = Handle<JSNumber>::dyn_vmcast(args.getThisHandle());
if (LLVM_UNLIKELY(!numPtr)) {
return runtime.raiseTypeError(
"Number.prototype.toFixed() can only be used on Number");
}
x = numPtr->getPrimitiveNumber();
}
if (std::isnan(x)) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::NaN));
}
// Account for very large numbers.
if (std::abs(x) >= 1e21) {
// toString(x) if abs(x) >= 10^21.
auto resultRes = toString_RJS(
runtime, runtime.makeHandle(HermesValue::encodeDoubleValue(x)));
if (LLVM_UNLIKELY(resultRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
return resultRes->getHermesValue();
}
// If negative, set a flag, proceed as if positive, and add a '-' later.
bool negative = false;
if (x < 0) {
negative = true;
x = -x;
}
// Decimal point index.
int decPt;
// 1 if negative, 0 else.
int sign;
// Points to the end of the string s after it's populated.
char *sEnd;
// 0 <= x < 10e21 now.
// Let n be an integer such that n/(10^f) - x is close to 0.
// Store the string representation of n, as provided by dtoa.
// Use mode=3 and precision=f for the dtoa call (fixed precision dtoa).
llvh::SmallString<32> n;
{
DtoaAllocator<> dalloc{};
char *s = ::dtoa_fixedpoint(dalloc, x, 3, f, &decPt, &sign, &sEnd);
n.append(s, sEnd);
::g_freedtoa(dalloc, s);
}
// Minimum number of digits required by the specified fixed-point length.
size_t minNLen = decPt + f;
// Pad n to account for for the specified fixed-point length.
while (n.size() < minNLen) {
n.push_back('0');
}
// Final string.
llvh::SmallString<32> m{};
// Check if n is 0 (n is empty or has no non-'0' characters);
bool isZero = n.find_first_not_of('0') == llvh::StringRef::npos;
if (isZero) {
// n is zero, so just add '0' and be done.
m.push_back('0');
} else {
// n is non-zero, so add all of n to m.
m.append(n);
}
if (f != 0) {
int32_t k = m.size();
if (k <= f) {
// Need leading zeroes after the decimal place if there aren't enough
// digits after the decimal place.
// Insert f+1-k occurrences of '0' to ensure we have enough digits.
for (int32_t i = 0; i < f + 1 - k; ++i) {
m.insert(m.begin(), '0');
}
// Update k to the new size.
k = f + 1;
}
// Place the decimal point after the first k-f characters of m,
// ensuring we have f digits after the decimal point.
m.insert(m.begin() + (k - f), '.');
}
if (negative) {
m.insert(m.begin(), '-');
}
return StringPrimitive::create(runtime, m);
}
CallResult<HermesValue>
numberPrototypeToExponential(void *, Runtime &runtime, NativeArgs args) {
// The number to make a string toExponential.
double x;
if (args.getThisArg().isNumber()) {
x = args.getThisArg().getNumber();
} else {
auto numPtr = Handle<JSNumber>::dyn_vmcast(args.getThisHandle());
if (LLVM_UNLIKELY(!numPtr)) {
return runtime.raiseTypeError(
"Number.prototype.toExponential() can only be used on Number");
}
x = numPtr->getPrimitiveNumber();
}
auto res = toIntegerOrInfinity(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double fDouble = res->getNumber();
if (std::isnan(x)) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::NaN));
}
if (x == std::numeric_limits<double>::infinity()) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::Infinity));
}
if (x == -std::numeric_limits<double>::infinity()) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::NegativeInfinity));
}
// 8. If f < 0 or f > 100, throw a RangeError exception.
if (LLVM_UNLIKELY(
!args.getArg(0).isUndefined() && (fDouble < 0 || fDouble > 100))) {
return runtime.raiseRangeError(
"toExponential argument must be between 0 and 100");
}
/// Number of digits after the decimal point.
/// Because we checked, 0 <= f <= 20.
/// In particular, we know that f is non-negative.
int f = static_cast<int>(fDouble);
// If negative, set a flag, proceed as if positive, and add a '-' later.
bool negative = false;
if (x < 0) {
negative = true;
x = -x;
}
// Final result.
llvh::SmallString<32> n{};
// The exponent in final string.
int e;
if (x == 0) {
// Implement the spec as in ES6, which most other implementations do.
// The ES5.1 spec says to output "0" regardless of the provided f value,
// but ES6 does not.
// Add trailing 0s to account for the supplied f,
// allowing for returning, e.g. "0.000e+0".
for (int i = 0; i < f + 1; ++i) {
n.push_back('0');
}
e = 0;
} else {
// x != 0
// Decimal point index.
int decPt;
// 1 if negative, 0 else.
int sign;
// Points to the end of the string s after it's populated.
char *sEnd;
DtoaAllocator<> dalloc{};
if (!args.getArg(0).isUndefined()) {
// Store the string representation of n, as provided by dtoa.
// Use mode=2 and precision=f+1 for the dtoa call (precision dtoa).
// Precision is f+1 to account for digit in front of the decimal point.
char *s = ::dtoa_fixedpoint(dalloc, x, 2, f + 1, &decPt, &sign, &sEnd);
n.append(s, sEnd);
::g_freedtoa(dalloc, s);
// Minimum length of the string should be enough to account for
// f digits after the decimal point, and 1 digit before it.
size_t minNLen = f + 1;
while (n.size() < minNLen) {
n.push_back('0');
}
} else {
// Store the string representation of n, as provided by dtoa.
// We use the default version of dtoa, so we get the shortest string.
// mode=0 and precision=0 give the shortest string.
char *s = ::g_dtoa(dalloc, x, 0, 0, &decPt, &sign, &sEnd);
n.append(s, sEnd);
::g_freedtoa(dalloc, s);
// All but the first digit of n will be after the decimal point.
f = n.size() - 1;
}
// We want the exponent to be one less than the position of the decPt.
// Example: 123.45 (f=2) has n=12345, decPt=3, so we want e=2.
e = decPt - 1;
}
if (f != 0) {
// This is valid because there are a non-zero number of digits after the
// decimal point.
n.insert(n.begin() + 1, '.');
}
// Append the exponent, including the '+' sign if positive.
if (e == 0) {
n.append("e+0");
} else {
llvh::raw_svector_ostream os{n};
os << llvh::format("e%+d", e);
}
if (negative) {
n.insert(n.begin(), '-');
}
return runtime.ignoreAllocationFailure(StringPrimitive::create(runtime, n));
}
CallResult<HermesValue>
numberPrototypeToPrecision(void *, Runtime &runtime, NativeArgs args) {
// The number to make a string toPrecision.
double x;
if (args.getThisArg().isNumber()) {
x = args.getThisArg().getNumber();
} else {
auto numPtr = Handle<JSNumber>::dyn_vmcast(args.getThisHandle());
if (LLVM_UNLIKELY(!numPtr)) {
return runtime.raiseTypeError(
"Number.prototype.toPrecision() can only be used on Number");
}
x = numPtr->getPrimitiveNumber();
}
if (args.getArg(0).isUndefined()) {
auto xHandle = runtime.makeHandle(HermesValue::encodeDoubleValue(x));
auto resultRes = toString_RJS(runtime, xHandle);
if (LLVM_UNLIKELY(resultRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
return resultRes->getHermesValue();
}
auto intRes = toIntegerOrInfinity(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(intRes == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double pDouble = intRes->getNumber();
if (std::isnan(x)) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::NaN));
}
if (x == std::numeric_limits<double>::infinity()) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::Infinity));
}
if (x == -std::numeric_limits<double>::infinity()) {
return HermesValue::encodeStringValue(
runtime.getPredefinedString(Predefined::NegativeInfinity));
}
// 8. If p < 1 or p > 100, throw a RangeError exception.
if (pDouble < 1 || pDouble > 100) {
return runtime.raiseRangeError(
"toPrecision argument must be between 1 and 100");
}
/// Number of significant digits in the result.
/// Because we checked, 1 <= p <= 21.
/// In particular, we know that p is non-negative.
int p = static_cast<int>(pDouble);
// If negative, set a flag, proceed as if positive, and add a '-' later.
bool negative = false;
if (x < 0) {
negative = true;
x = -x;
}
// Final result: add '-' to the start if x was negative.
llvh::SmallString<32> n{};
// The exponent in final string.
int e;
if (x == 0) {
// Add trailing 0s to account for the supplied p.
for (int i = 0; i < p; ++i) {
n.push_back('0');
}
e = 0;
} else {
// x != 0
// Decimal point index.
int decPt;
// 1 if negative, 0 else.
int sign;
// Points to the end of the string s after it's populated.
char *sEnd;
// Store the string representation of n, as provided by dtoa.
// Use mode=2 and precision=p for the dtoa call (precision dtoa).
{
DtoaAllocator<> dalloc{};
char *s = ::dtoa_fixedpoint(dalloc, x, 2, p, &decPt, &sign, &sEnd);
n.append(s, sEnd);
::g_freedtoa(dalloc, s);
}
// Minimum length of the string should be enough to account for
// p significant digits.
size_t minNLen = p;
while (n.size() < minNLen) {
n.push_back('0');
}
// We want the exponent to be one less than the position of the decPt.
// Example: 123.45 (p=2) has n=12345, decPt=3, so we want e=2.
e = decPt - 1;
if (e < -6 || e >= p) {
// Use scientific notation since the exponent is too big/small.
if (n.size() > 1) {
// Add a decimal point if there's enough digits to require it.
n.insert(n.begin() + 1, '.');
}
// Append exponent.
if (e == 0) {
n.append("e+0");
} else {
llvh::raw_svector_ostream os{n};
os << llvh::format("e%+d", e);
}
// ES5.1 spec says not to return here, and just set m.
// However, ES6 fixes this bug, which would result in the conditionals
// below executing even after we generated scientific notation,
// by telling us to return here instead.
if (negative) {
n.insert(n.begin(), '-');
}
return runtime.ignoreAllocationFailure(
StringPrimitive::create(runtime, n));
}
}
// From this point on, we know that -6 <= e < p <= n.size().
if (e == p - 1) {
if (negative) {
n.insert(n.begin(), '-');
}
return runtime.ignoreAllocationFailure(StringPrimitive::create(runtime, n));
}
// Now we know that -6 <= e < p-1, handle the cases that we need to put a
// decimal place.
if (e >= 0) {
n.insert(n.begin() + (e + 1), '.');
if (negative) {
n.insert(n.begin(), '-');
}
return runtime.ignoreAllocationFailure(StringPrimitive::create(runtime, n));
} else {
// Make a new string m here since it's easier than inserting at the start of
// n repeatedly.
llvh::SmallString<32> m{"0."};
m.reserve(2 + -(e + 1) + n.size());
for (int i = 0; i < -(e + 1); ++i) {
m.push_back('0');
}
m.append(n);
if (negative) {
m.insert(m.begin(), '-');
}
return runtime.ignoreAllocationFailure(StringPrimitive::create(runtime, m));
}
}
} // namespace vm
} // namespace hermes