lib/VM/JSLib/Math.cpp (407 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.8 Populate the Math object.
//===----------------------------------------------------------------------===//
#include "JSLibInternal.h"
#include "hermes/VM/JSLib/RuntimeCommonStorage.h"
#include "hermes/VM/Operations.h"
#include "hermes/VM/SingleObject.h"
#include "hermes/VM/StringPrimitive.h"
#define _USE_MATH_DEFINES
#include <float.h>
#include <math.h>
#include <random>
#include "hermes/Support/Math.h"
#include "hermes/Support/OSCompat.h"
#include "llvh/Support/MathExtras.h"
namespace hermes {
namespace vm {
/// @name Math
/// @{
/// @}
//===----------------------------------------------------------------------===//
/// Math.
// Implementation of Math.round(), following ES 5.1 15.8.2.15
// This cannot be a simple call to std::round() because std::round() rounds
// halfways away from zero, while Math.round must round towards positive
// infinity.
// The essential algorithm is floor(x + 0.5). However this has three
// complications:
// 1. The range [-.5, -0] must round to -0, not +0
// 2. The largest value less than 0.5, when added to 0.5, becomes 1.0
// (precision loss), causing us to round to 1 and not 0.
// 3. Above a certain threshold (shown below), x + 0.5 is the same as x + 1.0
// (precision loss), causing us to round too high.
// We handle this by checking explicitly for the problematic ranges.
static double roundHalfwaysTowardsInfinity(double x) {
// The first integer where all larger values are also integral
// The -1 is to account for the implicit (hidden) bit in the mantissa
static constexpr double integer_threshold = 1LLU << (DBL_MANT_DIG - 1);
double absf = std::fabs(x);
if (absf >= integer_threshold) {
// x is necessarily already integral.
return x;
} else if (absf < 0.5) {
// x may have too much precision to add 0.5. Just round to +/- 0.
return std::copysign(0, x);
} else {
// Here we can apply the normal rounding algorithm, but we need to be
// careful about -0.5, which must round to -0.
return std::copysign(std::floor(x + 0.5), x);
}
}
/// The Math object has functions like sin, cos, exp, etc. Most take one
/// argument, a few take two arguments, min() and max() may take any number
/// of arguments, and random() takes none. Use context as a index to switch to
/// the corresponding c function.
enum class MathKind {
#define MATHFUNC_1ARG(name, func) name,
#include "MathStdFunctions.def"
#undef MATHFUNC_1ARG
Num1ArgKinds,
#define MATHFUNC_2ARG(name, func) name,
#include "MathStdFunctions.def"
#undef MATHFUNC_2ARG
Num2ArgKinds
};
// Implementation of 1-arg Math functions like sin or exp
// Interprets the ctx pointer as an enum to invoke the
// corresponding function with the first argument
CallResult<HermesValue>
runContextFunc1Arg(void *ctx, Runtime &runtime, NativeArgs args) {
typedef double (*Math1ArgFuncPtr)(double);
static Math1ArgFuncPtr math1ArgFuncs[] = {
#define MATHFUNC_1ARG(name, func) func,
#include "MathStdFunctions.def"
#undef MATHFUNC_1ARG
};
assert(
(uint64_t)ctx < (uint64_t)MathKind::Num1ArgKinds &&
"runContextFunc1Arg with wrong kind");
Math1ArgFuncPtr func = math1ArgFuncs[(uint64_t)ctx];
auto res = toNumber_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double arg = res->getNumber();
return HermesValue::encodeDoubleValue(func(arg));
}
// Implementation of 2-arg Math functions like pow and atan2
// Interprets the ctx pointer as an enum and invoke corresponding
// function with the first two arguments
CallResult<HermesValue>
runContextFunc2Arg(void *ctx, Runtime &runtime, NativeArgs args) {
typedef double (*Math2ArgFuncPtr)(double, double);
static Math2ArgFuncPtr math2ArgFuncs[] = {
#define MATHFUNC_2ARG(name, func) func,
#include "MathStdFunctions.def"
#undef MATHFUNC_2ARG
};
assert(
(uint64_t)ctx > (uint64_t)MathKind::Num1ArgKinds &&
(uint64_t)ctx < (uint64_t)MathKind::Num2ArgKinds &&
"runContextFunc1Arg with wrong kind");
Math2ArgFuncPtr func =
math2ArgFuncs[(uint64_t)ctx - (uint64_t)MathKind::Num1ArgKinds - 1];
auto res = toNumber_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double arg0 = res->getNumber();
res = toNumber_RJS(runtime, args.getArgHandle(1));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double arg1 = res->getNumber();
return HermesValue::encodeDoubleValue(func(arg0, arg1));
}
// ES5.1 15.8.2.11
CallResult<HermesValue> mathMax(void *, Runtime &runtime, NativeArgs args) {
double result = -std::numeric_limits<double>::infinity();
GCScopeMarkerRAII marker{runtime};
for (const Handle<> sarg : args.handles()) {
marker.flush();
auto res = toNumber_RJS(runtime, sarg);
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double arg = res->getNumber();
if (std::isnan(result)) {
continue;
} else if (std::isnan(arg)) {
result = std::numeric_limits<double>::quiet_NaN();
} else if (arg > result || std::signbit(arg) < std::signbit(result)) {
// signbit(arg) < signbit(result) => arg is at least +0, result at most -0
result = arg;
}
}
return HermesValue::encodeDoubleValue(result);
}
// ES5.1 15.8.2.12
CallResult<HermesValue> mathMin(void *, Runtime &runtime, NativeArgs args) {
double result = std::numeric_limits<double>::infinity();
GCScopeMarkerRAII marker{runtime};
for (const Handle<> sarg : args.handles()) {
marker.flush();
auto res = toNumber_RJS(runtime, sarg);
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double arg = res->getNumber();
if (std::isnan(result)) {
continue;
} else if (std::isnan(arg)) {
result = std::numeric_limits<double>::quiet_NaN();
} else if (arg < result || std::signbit(arg) > std::signbit(result)) {
// signbit(arg) > signbit(result) => arg is at most -0, result at least +0
result = arg;
}
}
return HermesValue::encodeDoubleValue(result);
}
// ES9.0 20.2.2.26
CallResult<HermesValue> mathPow(void *, Runtime &runtime, NativeArgs args) {
auto res = toNumber_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
const double x = res->getNumber();
res = toNumber_RJS(runtime, args.getArgHandle(1));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
const double y = res->getNumber();
return HermesValue::encodeNumberValue(expOp(x, y));
}
// ES5.1 15.8.2.14
// Returns a Hermes-encoded pseudo-random number uniformly chosen from [0, 1)
CallResult<HermesValue> mathRandom(void *, Runtime &runtime, NativeArgs) {
RuntimeCommonStorage *storage = runtime.getCommonStorage();
if (!storage->randomEngineSeeded_) {
std::minstd_rand::result_type seed;
if (storage->env) {
if (storage->env->mathRandomSeed == 0) {
return runtime.raiseTypeError(
"Replay of Math.random() without a traced seed set");
}
seed = storage->env->mathRandomSeed;
} else {
seed = std::random_device()();
}
storage->randomEngine_.seed(seed);
storage->randomEngineSeeded_ = true;
if (LLVM_UNLIKELY(storage->shouldTrace)) {
// Math.random() is a source of unpredictable behavior in JS, which needs
// to be mocked for synthetic benchmarks.
storage->tracedEnv.mathRandomSeed = seed;
}
}
std::uniform_real_distribution<> dist(0.0, 1.0);
return HermesValue::encodeDoubleValue(dist(storage->randomEngine_));
}
CallResult<HermesValue> mathFround(void *, Runtime &runtime, NativeArgs args)
LLVM_NO_SANITIZE("float-cast-overflow");
CallResult<HermesValue> mathFround(void *, Runtime &runtime, NativeArgs args) {
auto res = toNumber_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double x = res->getNumber();
// Make the double x into a 32-bit float,
// and then recast it back to a 64-bit float to return it.
// This is UB for values outside of the range of a float, but this works on
// our current compilers.
// TODO(T43892577): Find an alternative that doesn't use UB (or validate that
// the UB is ok).
return HermesValue::encodeNumberValue(
static_cast<double>(static_cast<float>(x)));
}
// ES2022 21.3.2.18
CallResult<HermesValue> mathHypot(void *, Runtime &runtime, NativeArgs args) {
GCScope gcScope{runtime};
// 1. Let coerced be a new empty List.
llvh::SmallVector<double, 4> coerced{};
coerced.reserve(args.getArgCount());
// Store the max abs(arg), since every argument will be squared anyway.
// We scale down every argument by max while doing addition and sqrt,
// and then multiply by max at the end.
double max = 0;
bool hasNaN = false, hasInf = false;
auto marker = gcScope.createMarker();
// 2. For each element arg of args, do
for (const Handle<> arg : args.handles()) {
gcScope.flushToMarker(marker);
// a. Let n be ? ToNumber(arg).
auto res = toNumber_RJS(runtime, arg);
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double value = res->getNumber();
hasInf = std::isinf(value) || hasInf;
hasNaN = std::isnan(value) || hasNaN;
// b. Append n to coerced.
coerced.push_back(value);
max = std::max(std::fabs(value), max);
}
// 3. For each element number of coerced, do
// a. If number is +∞𝔽 or number is -∞𝔽, return +∞𝔽.
if (hasInf)
return HermesValue::encodeNumberValue(
std::numeric_limits<double>::infinity());
// 5. For each element number of coerced, do
// a. If number is NaN, return NaN.
if (hasNaN)
return HermesValue::encodeNaNValue();
assert(!(max < 0) && "max must not be negative (max(abs(value))");
// 6. If onlyZero is true, return +0𝔽.
if (max == 0) {
return HermesValue::encodeNumberValue(+0);
}
// 7. Return an implementation-approximated Number value representing the
// square root of the sum of squares of the mathematical values of the
// elements of coerced.
// We use the Kahan summation algorithm, since we are supposed to
// "take care to avoid the loss of precision from overflows and underflows".
// We add (value / max)**2 each iteration through the loop,
// so that multiplying by max following the sqrt will negate
// its effects. This normalizes the values to allow more accurate summation.
double sum = 0;
double c = 0;
for (const double value : coerced) {
double addend = (value / max) * (value / max);
// Perform Kahan summation and put the result and compensation in sum and c.
double y = addend - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
double result = std::sqrt(sum) * max;
return HermesValue::encodeNumberValue(result);
}
// ES6.0 20.2.2.19
// Integer multiplication.
CallResult<HermesValue> mathImul(void *, Runtime &runtime, NativeArgs args) {
auto res = toUInt32_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
uint32_t a = res->getNumber();
res = toUInt32_RJS(runtime, args.getArgHandle(1));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
uint32_t b = res->getNumber();
// Compute a * b mod 2^32.
uint32_t product = a * b;
// If product >= 2^31, return product - 2^32, else return product.
return HermesValue::encodeNumberValue(static_cast<int32_t>(product));
}
// ES6.0 20.2.2.11
// Count leading zeros on the 32-bit number.
CallResult<HermesValue> mathClz32(void *, Runtime &runtime, NativeArgs args) {
auto res = toUInt32_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
uint32_t n = res->getNumberAs<uint32_t>();
uint32_t p = llvh::countLeadingZeros(n);
return HermesValue::encodeNumberValue(p);
}
// ES6.0 20.2.2.29
// Get the sign of the input.
CallResult<HermesValue> mathSign(void *, Runtime &runtime, NativeArgs args) {
auto res = toNumber_RJS(runtime, args.getArgHandle(0));
if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
return ExecutionStatus::EXCEPTION;
}
double x = res->getNumber();
if (std::isnan(x)) {
return HermesValue::encodeNaNValue();
}
if (x == 0) {
// Preserve sign bit: return -0 for x == -0 and +0 for x == +0.
return HermesValue::encodeNumberValue(x);
}
return HermesValue::encodeNumberValue(std::signbit(x) ? -1 : +1);
}
Handle<JSObject> createMathObject(Runtime &runtime) {
auto objRes = JSMath::create(
runtime, Handle<JSObject>::vmcast(&runtime.objectPrototype));
assert(objRes != ExecutionStatus::EXCEPTION && "unable to define Math");
auto math = runtime.makeHandle<JSMath>(*objRes);
DefinePropertyFlags constantDPF =
DefinePropertyFlags::getDefaultNewPropertyFlags();
constantDPF.enumerable = 0;
constantDPF.writable = 0;
constantDPF.configurable = 0;
MutableHandle<> numberHandle{runtime};
// ES5.1 15.8.1, Math value properties
auto setMathValueProperty = [&](SymbolID name, double value) {
numberHandle = HermesValue::encodeNumberValue(value);
auto result = JSObject::defineOwnProperty(
math, runtime, name, constantDPF, numberHandle);
assert(
result != ExecutionStatus::EXCEPTION &&
"defineOwnProperty() failed on a new object");
(void)result;
};
setMathValueProperty(Predefined::getSymbolID(Predefined::E), M_E);
setMathValueProperty(Predefined::getSymbolID(Predefined::LN10), M_LN10);
setMathValueProperty(Predefined::getSymbolID(Predefined::LN2), M_LN2);
setMathValueProperty(Predefined::getSymbolID(Predefined::LOG2E), M_LOG2E);
setMathValueProperty(Predefined::getSymbolID(Predefined::LOG10E), M_LOG10E);
setMathValueProperty(Predefined::getSymbolID(Predefined::PI), M_PI);
setMathValueProperty(Predefined::getSymbolID(Predefined::SQRT1_2), M_SQRT1_2);
setMathValueProperty(Predefined::getSymbolID(Predefined::SQRT2), M_SQRT2);
// ES5.1 15.8.2, Math function properties
auto setMathFunctionProperty1Arg = [&runtime, math](
SymbolID name, MathKind kind) {
defineMethod(runtime, math, name, (void *)kind, runContextFunc1Arg, 1);
};
auto setMathFunctionProperty2Arg = [&runtime, math](
SymbolID name, MathKind kind) {
defineMethod(runtime, math, name, (void *)kind, runContextFunc2Arg, 2);
};
// We use the C versions of some of these functions from <math.h>
// because on Android, the C++ <cmath> library doesn't have them.
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::abs), MathKind::abs);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::acos), MathKind::acos);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::acosh), MathKind::acosh);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::asin), MathKind::asin);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::asinh), MathKind::asinh);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::atan), MathKind::atan);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::atanh), MathKind::atanh);
setMathFunctionProperty2Arg(
Predefined::getSymbolID(Predefined::atan2), MathKind::atan2);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::cbrt), MathKind::cbrt);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::ceil), MathKind::ceil);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::clz32),
nullptr,
mathClz32,
1);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::cos), MathKind::cos);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::cosh), MathKind::cosh);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::exp), MathKind::exp);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::expm1), MathKind::expm1);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::floor), MathKind::floor);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::fround),
nullptr,
mathFround,
1);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::hypot),
nullptr,
mathHypot,
2);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::log), MathKind::log);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::log10), MathKind::log10);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::log1p), MathKind::log1p);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::log2), MathKind::log2);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::trunc), MathKind::trunc);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::max),
nullptr,
mathMax,
2);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::min),
nullptr,
mathMin,
2);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::imul),
nullptr,
mathImul,
2);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::pow),
nullptr,
mathPow,
2);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::random),
nullptr,
mathRandom,
0);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::round), MathKind::round);
defineMethod(
runtime,
math,
Predefined::getSymbolID(Predefined::sign),
nullptr,
mathSign,
1);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::sin), MathKind::sin);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::sinh), MathKind::sinh);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::sqrt), MathKind::sqrt);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::tan), MathKind::tan);
setMathFunctionProperty1Arg(
Predefined::getSymbolID(Predefined::tanh), MathKind::tanh);
auto dpf = DefinePropertyFlags::getDefaultNewPropertyFlags();
dpf.writable = 0;
dpf.enumerable = 0;
defineProperty(
runtime,
math,
Predefined::getSymbolID(Predefined::SymbolToStringTag),
runtime.getPredefinedStringHandle(Predefined::Math),
dpf);
return math;
}
} // namespace vm
} // namespace hermes