LibJS: Use NaN boxing to decrease the memory size of Values

Using the fact that there are 2^52-2 NaN representations we can
"NaN-box" all the Values possible. This means that Value no longer has
an explicit "Type" but that information is now stored in the bits of a
double. This is done by "tagging" the top two bytes of the double.
For a full explanation see the large comment with asserts at the top of
Value.

We can also use the exact representation of the tags to make checking
properties like nullish, or is_cell quicker. But the largest gains are
in the fact that the size of a Value is now halved.

The SunSpider and other benchmarks have been ran to confirm that there
are no regressions in performance compared to the previous
implementation. The tests never performed worse and in some cases
performed better. But the biggest differences can be seen in memory
usage when large arrays are allocated. A simple test which allocates a
1000 arrays of size 100000 has roughly half the memory usage.

There is also space in the representations for future expansions such as
tuples and records.

To ensure that Values on the stack and registers are not lost during
garbage collection we also have to add a check to the Heap to check for
any of the cell tags and extracting the canonical form of the pointer
if it matches.
This commit is contained in:
davidot 2022-02-25 01:26:52 +01:00 committed by Andreas Kling
parent 6c504e2bff
commit e746360b9a
Notes: sideshowbarker 2024-07-17 08:13:53 +09:00
7 changed files with 391 additions and 259 deletions

View File

@ -324,7 +324,7 @@ JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_arithmetic)
auto& column_name_str = column_name.as_string().string();
auto offset = TRY(vm.argument(1).to_number(global_object));
auto offset_number = offset.as_i32();
auto offset_number = static_cast<i32>(offset.as_double());
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));

View File

@ -356,7 +356,7 @@ static ThrowCompletionOr<void> argument_list_evaluation(Interpreter& interpreter
for (auto& argument : arguments) {
auto value = TRY(argument.value->execute(interpreter, global_object)).release_value();
if (argument.is_spread) {
auto result = TRY(get_iterator_values(global_object, value, [&](Value iterator_value) -> Optional<Completion> {
TRY(get_iterator_values(global_object, value, [&](Value iterator_value) -> Optional<Completion> {
list.append(iterator_value);
return {};
}));

View File

@ -136,15 +136,32 @@ __attribute__((no_sanitize("address"))) void Heap::gather_conservative_roots(Has
auto* raw_jmp_buf = reinterpret_cast<FlatPtr const*>(buf);
auto add_possible_value = [&](FlatPtr data) {
if constexpr (sizeof(FlatPtr*) == sizeof(Value)) {
// Because Value stores pointers in non-canonical form we have to check if the top bytes
// match any pointer-backed tag, in that case we have to extract the pointer to its
// canonical form and add that as a possible pointer.
if ((data & SHIFTED_IS_CELL_PATTERN) == SHIFTED_IS_CELL_PATTERN)
possible_pointers.set((u64)(((i64)data << 16) >> 16));
else
possible_pointers.set(data);
} else {
static_assert((sizeof(Value) % sizeof(FlatPtr*)) == 0);
// In the 32-bit case we will look at the top and bottom part of Value separately we just
// add both the upper and lower bytes as possible pointers.
possible_pointers.set(data);
}
};
for (size_t i = 0; i < ((size_t)sizeof(buf)) / sizeof(FlatPtr); i += sizeof(FlatPtr))
possible_pointers.set(raw_jmp_buf[i]);
add_possible_value(raw_jmp_buf[i]);
auto stack_reference = bit_cast<FlatPtr>(&dummy);
auto& stack_info = m_vm.stack_info();
for (FlatPtr stack_address = stack_reference; stack_address < stack_info.top(); stack_address += sizeof(FlatPtr)) {
auto data = *reinterpret_cast<FlatPtr*>(stack_address);
possible_pointers.set(data);
add_possible_value(data);
}
HashTable<HeapBlock*> all_live_heap_blocks;

View File

@ -33,7 +33,7 @@ public:
if (value.is_symbol())
return PropertyKey { value.as_symbol() };
if (value.is_integral_number() && value.as_double() >= 0 && value.as_double() < NumericLimits<u32>::max())
return value.as_u32();
return static_cast<u32>(value.as_double());
return TRY(value.to_string(global_object));
}

View File

@ -41,10 +41,16 @@ namespace JS {
static inline bool same_type_for_equality(Value const& lhs, Value const& rhs)
{
if (lhs.type() == rhs.type())
// If the top two bytes are identical then either:
// both are NaN boxed Values with the same type
// or they are doubles which happen to have the same top bytes.
if ((lhs.encoded() & TAG_EXTRACTION) == (rhs.encoded() & TAG_EXTRACTION))
return true;
if (lhs.is_number() && rhs.is_number())
return true;
// One of the Values is not a number and they do not have the same tag
return false;
}
@ -210,7 +216,7 @@ ThrowCompletionOr<bool> Value::is_array(GlobalObject& global_object) const
Array& Value::as_array()
{
VERIFY(is_object() && is<Array>(as_object()));
return static_cast<Array&>(*m_value.as_object);
return static_cast<Array&>(as_object());
}
// 7.2.3 IsCallable ( argument ), https://tc39.es/ecma262/#sec-iscallable
@ -263,28 +269,28 @@ ThrowCompletionOr<bool> Value::is_regexp(GlobalObject& global_object) const
// 13.5.3 The typeof Operator, https://tc39.es/ecma262/#sec-typeof-operator
String Value::typeof() const
{
switch (m_type) {
case Value::Type::Undefined:
return "undefined";
case Value::Type::Null:
return "object";
case Value::Type::Int32:
case Value::Type::Double:
if (is_number())
return "number";
case Value::Type::String:
switch (m_value.tag) {
case UNDEFINED_TAG:
return "undefined";
case NULL_TAG:
return "object";
case STRING_TAG:
return "string";
case Value::Type::Object:
case OBJECT_TAG:
// B.3.7.3 Changes to the typeof Operator, https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot-typeof
if (as_object().is_htmldda())
return "undefined";
if (is_function())
return "function";
return "object";
case Value::Type::Boolean:
case BOOLEAN_TAG:
return "boolean";
case Value::Type::Symbol:
case SYMBOL_TAG:
return "symbol";
case Value::Type::BigInt:
case BIGINT_TAG:
return "bigint";
default:
VERIFY_NOT_REACHED();
@ -293,26 +299,27 @@ String Value::typeof() const
String Value::to_string_without_side_effects() const
{
switch (m_type) {
case Type::Undefined:
return "undefined";
case Type::Null:
return "null";
case Type::Boolean:
return m_value.as_bool ? "true" : "false";
case Type::Int32:
return String::number(m_value.as_i32);
case Type::Double:
if (is_double())
return double_to_string(m_value.as_double);
case Type::String:
return m_value.as_string->string();
case Type::Symbol:
return m_value.as_symbol->to_string();
case Type::BigInt:
return m_value.as_bigint->to_string();
case Type::Object:
switch (m_value.tag) {
case UNDEFINED_TAG:
return "undefined";
case NULL_TAG:
return "null";
case BOOLEAN_TAG:
return as_bool() ? "true" : "false";
case INT32_TAG:
return String::number(as_i32());
case STRING_TAG:
return as_string().string();
case SYMBOL_TAG:
return as_symbol().to_string();
case BIGINT_TAG:
return as_bigint().to_string();
case OBJECT_TAG:
return String::formatted("[object {}]", as_object().class_name());
case Type::Accessor:
case ACCESSOR_TAG:
return "<accessor>";
default:
VERIFY_NOT_REACHED();
@ -330,25 +337,26 @@ ThrowCompletionOr<PrimitiveString*> Value::to_primitive_string(GlobalObject& glo
// 7.1.17 ToString ( argument ), https://tc39.es/ecma262/#sec-tostring
ThrowCompletionOr<String> Value::to_string(GlobalObject& global_object) const
{
auto& vm = global_object.vm();
switch (m_type) {
case Type::Undefined:
return "undefined"sv;
case Type::Null:
return "null"sv;
case Type::Boolean:
return m_value.as_bool ? "true"sv : "false"sv;
case Type::Int32:
return String::number(m_value.as_i32);
case Type::Double:
if (is_double())
return double_to_string(m_value.as_double);
case Type::String:
return m_value.as_string->string();
case Type::Symbol:
auto& vm = global_object.vm();
switch (m_value.tag) {
case UNDEFINED_TAG:
return "undefined"sv;
case NULL_TAG:
return "null"sv;
case BOOLEAN_TAG:
return as_bool() ? "true"sv : "false"sv;
case INT32_TAG:
return String::number(as_i32());
case STRING_TAG:
return as_string().string();
case SYMBOL_TAG:
return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "symbol", "string");
case Type::BigInt:
return m_value.as_bigint->big_integer().to_base(10);
case Type::Object: {
case BIGINT_TAG:
return as_bigint().big_integer().to_base(10);
case OBJECT_TAG: {
auto primitive_value = TRY(to_primitive(global_object, PreferredType::String));
return primitive_value.to_string(global_object);
}
@ -359,8 +367,8 @@ ThrowCompletionOr<String> Value::to_string(GlobalObject& global_object) const
ThrowCompletionOr<Utf16String> Value::to_utf16_string(GlobalObject& global_object) const
{
if (m_type == Type::String)
return m_value.as_string->utf16_string();
if (is_string())
return as_string().utf16_string();
auto utf8_string = TRY(to_string(global_object));
return Utf16String(utf8_string);
@ -369,27 +377,29 @@ ThrowCompletionOr<Utf16String> Value::to_utf16_string(GlobalObject& global_objec
// 7.1.2 ToBoolean ( argument ), https://tc39.es/ecma262/#sec-toboolean
bool Value::to_boolean() const
{
switch (m_type) {
case Type::Undefined:
case Type::Null:
return false;
case Type::Boolean:
return m_value.as_bool;
case Type::Int32:
return m_value.as_i32 != 0;
case Type::Double:
if (is_double()) {
if (is_nan())
return false;
return m_value.as_double != 0;
case Type::String:
return !m_value.as_string->is_empty();
case Type::Symbol:
}
switch (m_value.tag) {
case UNDEFINED_TAG:
case NULL_TAG:
return false;
case BOOLEAN_TAG:
return as_bool();
case INT32_TAG:
return as_i32() != 0;
case STRING_TAG:
return !as_string().is_empty();
case SYMBOL_TAG:
return true;
case Type::BigInt:
return m_value.as_bigint->big_integer() != BIGINT_ZERO;
case Type::Object:
case BIGINT_TAG:
return as_bigint().big_integer() != BIGINT_ZERO;
case OBJECT_TAG:
// B.3.7.1 Changes to ToBoolean, https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot-to-boolean
if (m_value.as_object->is_htmldda())
if (as_object().is_htmldda())
return false;
return true;
default:
@ -432,22 +442,23 @@ ThrowCompletionOr<Value> Value::to_primitive(GlobalObject& global_object, Prefer
// 7.1.18 ToObject ( argument ), https://tc39.es/ecma262/#sec-toobject
ThrowCompletionOr<Object*> Value::to_object(GlobalObject& global_object) const
{
switch (m_type) {
case Type::Undefined:
case Type::Null:
return global_object.vm().throw_completion<TypeError>(global_object, ErrorType::ToObjectNullOrUndefined);
case Type::Boolean:
return BooleanObject::create(global_object, m_value.as_bool);
case Type::Int32:
case Type::Double:
VERIFY(!is_empty());
if (is_number())
return NumberObject::create(global_object, as_double());
case Type::String:
return StringObject::create(global_object, *m_value.as_string, *global_object.string_prototype());
case Type::Symbol:
return SymbolObject::create(global_object, *m_value.as_symbol);
case Type::BigInt:
return BigIntObject::create(global_object, *m_value.as_bigint);
case Type::Object:
switch (m_value.tag) {
case UNDEFINED_TAG:
case NULL_TAG:
return global_object.vm().throw_completion<TypeError>(global_object, ErrorType::ToObjectNullOrUndefined);
case BOOLEAN_TAG:
return BooleanObject::create(global_object, as_bool());
case STRING_TAG:
return StringObject::create(global_object, const_cast<JS::PrimitiveString&>(as_string()), *global_object.string_prototype());
case SYMBOL_TAG:
return SymbolObject::create(global_object, const_cast<JS::Symbol&>(as_symbol()));
case BIGINT_TAG:
return BigIntObject::create(global_object, const_cast<JS::BigInt&>(as_bigint()));
case OBJECT_TAG:
return &const_cast<Object&>(as_object());
default:
VERIFY_NOT_REACHED();
@ -466,17 +477,18 @@ FLATTEN ThrowCompletionOr<Value> Value::to_numeric(GlobalObject& global_object)
// 7.1.4 ToNumber ( argument ), https://tc39.es/ecma262/#sec-tonumber
ThrowCompletionOr<Value> Value::to_number(GlobalObject& global_object) const
{
switch (m_type) {
case Type::Undefined:
return js_nan();
case Type::Null:
return Value(0);
case Type::Boolean:
return Value(m_value.as_bool ? 1 : 0);
case Type::Int32:
case Type::Double:
VERIFY(!is_empty());
if (is_number())
return *this;
case Type::String: {
switch (m_value.tag) {
case UNDEFINED_TAG:
return js_nan();
case NULL_TAG:
return Value(0);
case BOOLEAN_TAG:
return Value(as_bool() ? 1 : 0);
case STRING_TAG: {
String string = Utf8View(as_string().string()).trim(whitespace_characters, AK::TrimMode::Both).as_string();
if (string.is_empty())
return Value(0);
@ -490,11 +502,11 @@ ThrowCompletionOr<Value> Value::to_number(GlobalObject& global_object) const
return js_nan();
return Value(parsed_double);
}
case Type::Symbol:
case SYMBOL_TAG:
return global_object.vm().throw_completion<TypeError>(global_object, ErrorType::Convert, "symbol", "number");
case Type::BigInt:
case BIGINT_TAG:
return global_object.vm().throw_completion<TypeError>(global_object, ErrorType::Convert, "BigInt", "number");
case Type::Object: {
case OBJECT_TAG: {
auto primitive = TRY(to_primitive(global_object, PreferredType::Number));
return primitive.to_number(global_object);
}
@ -508,22 +520,24 @@ ThrowCompletionOr<BigInt*> Value::to_bigint(GlobalObject& global_object) const
{
auto& vm = global_object.vm();
auto primitive = TRY(to_primitive(global_object, PreferredType::Number));
switch (primitive.type()) {
case Type::Undefined:
VERIFY(!primitive.is_empty());
if (primitive.is_number())
return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "number", "BigInt");
switch (primitive.m_value.tag) {
case UNDEFINED_TAG:
return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "undefined", "BigInt");
case Type::Null:
case NULL_TAG:
return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "null", "BigInt");
case Type::Boolean: {
case BOOLEAN_TAG: {
auto value = primitive.as_bool() ? 1 : 0;
return js_bigint(vm, Crypto::SignedBigInteger { value });
}
case Type::BigInt:
case BIGINT_TAG:
return &primitive.as_bigint();
case Type::Int32:
case Type::Double:
return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "number", "BigInt");
case Type::String: {
// 1. Let n be StringToBigInt(prim).
case STRING_TAG: {
// 1. Let n be ! StringToBigInt(prim).
auto bigint = primitive.string_to_bigint(global_object);
// 2. If n is undefined, throw a SyntaxError exception.
@ -533,7 +547,7 @@ ThrowCompletionOr<BigInt*> Value::to_bigint(GlobalObject& global_object) const
// 3. Return n.
return bigint.release_value();
}
case Type::Symbol:
case SYMBOL_TAG:
return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "symbol", "BigInt");
default:
VERIFY_NOT_REACHED();
@ -632,7 +646,7 @@ ThrowCompletionOr<double> Value::to_double(GlobalObject& global_object) const
// 7.1.19 ToPropertyKey ( argument ), https://tc39.es/ecma262/#sec-topropertykey
ThrowCompletionOr<PropertyKey> Value::to_property_key(GlobalObject& global_object) const
{
if (type() == Type::Int32 && as_i32() >= 0)
if (is_int32() && as_i32() >= 0)
return PropertyKey { as_i32() };
auto key = TRY(to_primitive(global_object, PreferredType::String));
if (key.is_symbol())
@ -642,7 +656,7 @@ ThrowCompletionOr<PropertyKey> Value::to_property_key(GlobalObject& global_objec
ThrowCompletionOr<i32> Value::to_i32_slow_case(GlobalObject& global_object) const
{
VERIFY(type() != Type::Int32);
VERIFY(!is_int32());
double value = TRY(to_number(global_object)).as_double();
if (!isfinite(value) || value == 0)
return 0;
@ -659,8 +673,8 @@ ThrowCompletionOr<i32> Value::to_i32_slow_case(GlobalObject& global_object) cons
ThrowCompletionOr<i32> Value::to_i32(GlobalObject& global_object) const
{
if (m_type == Type::Int32)
return m_value.as_i32;
if (is_int32())
return as_i32();
return to_i32_slow_case(global_object);
}
@ -872,7 +886,7 @@ ThrowCompletionOr<FunctionObject*> Value::get_method(GlobalObject& global_object
// 13.10 Relational Operators, https://tc39.es/ecma262/#sec-relational-operators
ThrowCompletionOr<Value> greater_than(GlobalObject& global_object, Value lhs, Value rhs)
{
if (lhs.type() == Value::Type::Int32 && rhs.type() == Value::Type::Int32)
if (lhs.is_int32() && rhs.is_int32())
return lhs.as_i32() > rhs.as_i32();
TriState relation = TRY(is_less_than(global_object, lhs, rhs, false));
@ -884,7 +898,7 @@ ThrowCompletionOr<Value> greater_than(GlobalObject& global_object, Value lhs, Va
// 13.10 Relational Operators, https://tc39.es/ecma262/#sec-relational-operators
ThrowCompletionOr<Value> greater_than_equals(GlobalObject& global_object, Value lhs, Value rhs)
{
if (lhs.type() == Value::Type::Int32 && rhs.type() == Value::Type::Int32)
if (lhs.is_int32() && rhs.is_int32())
return lhs.as_i32() >= rhs.as_i32();
TriState relation = TRY(is_less_than(global_object, lhs, rhs, true));
@ -896,7 +910,7 @@ ThrowCompletionOr<Value> greater_than_equals(GlobalObject& global_object, Value
// 13.10 Relational Operators, https://tc39.es/ecma262/#sec-relational-operators
ThrowCompletionOr<Value> less_than(GlobalObject& global_object, Value lhs, Value rhs)
{
if (lhs.type() == Value::Type::Int32 && rhs.type() == Value::Type::Int32)
if (lhs.is_int32() && rhs.is_int32())
return lhs.as_i32() < rhs.as_i32();
TriState relation = TRY(is_less_than(global_object, lhs, rhs, true));
@ -908,7 +922,7 @@ ThrowCompletionOr<Value> less_than(GlobalObject& global_object, Value lhs, Value
// 13.10 Relational Operators, https://tc39.es/ecma262/#sec-relational-operators
ThrowCompletionOr<Value> less_than_equals(GlobalObject& global_object, Value lhs, Value rhs)
{
if (lhs.type() == Value::Type::Int32 && rhs.type() == Value::Type::Int32)
if (lhs.is_int32() && rhs.is_int32())
return lhs.as_i32() <= rhs.as_i32();
TriState relation = TRY(is_less_than(global_object, lhs, rhs, false));
@ -1078,7 +1092,7 @@ ThrowCompletionOr<Value> unsigned_right_shift(GlobalObject& global_object, Value
ThrowCompletionOr<Value> add(GlobalObject& global_object, Value lhs, Value rhs)
{
if (both_number(lhs, rhs)) {
if (lhs.type() == Value::Type::Int32 && rhs.type() == Value::Type::Int32) {
if (lhs.is_int32() && rhs.is_int32()) {
Checked<i32> result;
result = MUST(lhs.to_i32(global_object));
result += MUST(rhs.to_i32(global_object));
@ -1112,8 +1126,12 @@ ThrowCompletionOr<Value> sub(GlobalObject& global_object, Value lhs, Value rhs)
auto& vm = global_object.vm();
auto lhs_numeric = TRY(lhs.to_numeric(global_object));
auto rhs_numeric = TRY(rhs.to_numeric(global_object));
if (both_number(lhs_numeric, rhs_numeric))
return Value(lhs_numeric.as_double() - rhs_numeric.as_double());
if (both_number(lhs_numeric, rhs_numeric)) {
double lhsd = lhs_numeric.as_double();
double rhsd = rhs_numeric.as_double();
double interm = lhsd - rhsd;
return Value(interm);
}
if (both_bigint(lhs_numeric, rhs_numeric))
return Value(js_bigint(vm, lhs_numeric.as_bigint().big_integer().minus(rhs_numeric.as_bigint().big_integer())));
return vm.throw_completion<TypeError>(global_object, ErrorType::BigIntBadOperatorOtherType, "subtraction");
@ -1182,7 +1200,7 @@ static Value exp_double(Value base, Value exponent)
if (base.is_positive_infinity())
return exponent.as_double() > 0 ? js_infinity() : Value(0);
if (base.is_negative_infinity()) {
auto is_odd_integral_number = exponent.is_integral_number() && (exponent.as_i32() % 2 != 0);
auto is_odd_integral_number = exponent.is_integral_number() && (static_cast<i32>(exponent.as_double()) % 2 != 0);
if (exponent.as_double() > 0)
return is_odd_integral_number ? js_negative_infinity() : js_infinity();
else
@ -1191,7 +1209,7 @@ static Value exp_double(Value base, Value exponent)
if (base.is_positive_zero())
return exponent.as_double() > 0 ? Value(0) : js_infinity();
if (base.is_negative_zero()) {
auto is_odd_integral_number = exponent.is_integral_number() && (exponent.as_i32() % 2 != 0);
auto is_odd_integral_number = exponent.is_integral_number() && (static_cast<i32>(exponent.as_double()) % 2 != 0);
if (exponent.as_double() > 0)
return is_odd_integral_number ? Value(-0.0) : Value(0);
else
@ -1342,21 +1360,10 @@ bool same_value_non_numeric(Value lhs, Value rhs)
VERIFY(!lhs.is_number() && !lhs.is_bigint());
VERIFY(same_type_for_equality(lhs, rhs));
switch (lhs.type()) {
case Value::Type::Undefined:
case Value::Type::Null:
return true;
case Value::Type::String:
if (lhs.is_string())
return lhs.as_string().string() == rhs.as_string().string();
case Value::Type::Symbol:
return &lhs.as_symbol() == &rhs.as_symbol();
case Value::Type::Boolean:
return lhs.as_bool() == rhs.as_bool();
case Value::Type::Object:
return &lhs.as_object() == &rhs.as_object();
default:
VERIFY_NOT_REACHED();
}
return lhs.m_value.encoded == rhs.m_value.encoded;
}
// 7.2.15 IsStrictlyEqual ( x, y ), https://tc39.es/ecma262/#sec-isstrictlyequal

View File

@ -28,40 +28,119 @@ static constexpr u64 NEGATIVE_ZERO_BITS = ((u64)1 << 63);
namespace JS {
static_assert(sizeof(double) == 8);
static_assert(sizeof(void*) == sizeof(double) || sizeof(void*) == sizeof(u32));
// To make our Value representation compact we can use the fact that IEEE
// doubles have a lot (2^52 - 2) of NaN bit patterns. The canonical form being
// just 0x7FF8000000000000 i.e. sign = 0 exponent is all ones and the top most
// bit of the mantissa set.
static constexpr u64 CANON_NAN_BITS = bit_cast<u64>(__builtin_nan(""));
static_assert(CANON_NAN_BITS == 0x7FF8000000000000);
// (Unfortunately all the other values are valid so we have to convert any
// incoming NaNs to this pattern although in practice it seems only the negative
// version of these CANON_NAN_BITS)
// +/- Infinity are represented by a full exponent but without any bits of the
// mantissa set.
static constexpr u64 POSITIVE_INFINITY_BITS = bit_cast<u64>(__builtin_huge_val());
static constexpr u64 NEGATIVE_INFINITY_BITS = bit_cast<u64>(-__builtin_huge_val());
static_assert(POSITIVE_INFINITY_BITS == 0x7FF0000000000000);
static_assert(NEGATIVE_INFINITY_BITS == 0xFFF0000000000000);
// However as long as any bit is set in the mantissa with the exponent of all
// ones this value is a NaN, and it even ignores the sign bit.
static_assert(isnan(bit_cast<double>(0x7FF0000000000001)));
static_assert(isnan(bit_cast<double>(0xFFF0000000040000)));
// This means we can use all of these NaNs to store all other options for Value.
// To make sure all of these other representations we use 0x7FF8 as the base top
// 2 bytes which ensures the value is always a NaN.
static constexpr u64 BASE_TAG = 0x7FF8;
// This leaves the sign bit and the three lower bits for tagging a value and then
// 48 bits of potential payload.
// First the pointer backed types (Object, String etc.), to signify this category
// and make stack scanning easier we use the sign bit (top most bit) of 1 to
// signify that it is a pointer backed type.
static constexpr u64 IS_CELL_BIT = 0x8000 | BASE_TAG;
// On all current 64-bit systems this code runs pointer actually only use the
// lowest 6 bytes which fits neatly into our NaN payload with the top two bytes
// left over for marking it as a NaN and tagging the type.
// Note that we do need to take care when extracting the pointer value but this
// is explained in the extract_pointer method.
// This leaves us 3 bits to tag the type of pointer:
static constexpr u64 OBJECT_TAG = 0b001 | IS_CELL_BIT;
static constexpr u64 STRING_TAG = 0b010 | IS_CELL_BIT;
static constexpr u64 SYMBOL_TAG = 0b011 | IS_CELL_BIT;
static constexpr u64 ACCESSOR_TAG = 0b100 | IS_CELL_BIT;
static constexpr u64 BIGINT_TAG = 0b101 | IS_CELL_BIT;
// We can then by extracting the top 13 bits quickly check if a Value is
// pointer backed.
static constexpr u64 IS_CELL_PATTERN = 0xFFF8ULL;
static_assert((OBJECT_TAG & IS_CELL_PATTERN) == IS_CELL_PATTERN);
static_assert((STRING_TAG & IS_CELL_PATTERN) == IS_CELL_PATTERN);
static_assert((CANON_NAN_BITS & IS_CELL_PATTERN) != IS_CELL_PATTERN);
static_assert((NEGATIVE_INFINITY_BITS & IS_CELL_PATTERN) != IS_CELL_PATTERN);
// Then for the non pointer backed types we don't set the sign bit and use the
// three lower bits for tagging as well.
static constexpr u64 UNDEFINED_TAG = 0b110 | BASE_TAG;
static constexpr u64 NULL_TAG = 0b111 | BASE_TAG;
static constexpr u64 BOOLEAN_TAG = 0b001 | BASE_TAG;
static constexpr u64 INT32_TAG = 0b010 | BASE_TAG;
static constexpr u64 EMPTY_TAG = 0b011 | BASE_TAG;
// Notice how only undefined and null have the top bit set, this mean we can
// quickly check for nullish values by checking if the top and bottom bits are set
// but the middle one isn't.
static constexpr u64 IS_NULLISH_EXTRACT_PATTERN = 0xFFFEULL;
static constexpr u64 IS_NULLISH_PATTERN = 0x7FFEULL;
static_assert((UNDEFINED_TAG & IS_NULLISH_EXTRACT_PATTERN) == IS_NULLISH_PATTERN);
static_assert((NULL_TAG & IS_NULLISH_EXTRACT_PATTERN) == IS_NULLISH_PATTERN);
static_assert((BOOLEAN_TAG & IS_NULLISH_EXTRACT_PATTERN) != IS_NULLISH_PATTERN);
static_assert((INT32_TAG & IS_NULLISH_EXTRACT_PATTERN) != IS_NULLISH_PATTERN);
static_assert((EMPTY_TAG & IS_NULLISH_EXTRACT_PATTERN) != IS_NULLISH_PATTERN);
// We also have the empty tag to represent array holes however since empty
// values are not valid anywhere else we can use this "value" to our advantage
// in Optional<Value> to represent the empty optional.
static constexpr u64 TAG_EXTRACTION = 0xFFFF000000000000;
static constexpr u64 TAG_SHIFT = 48;
static constexpr u64 SHIFTED_INT32_TAG = INT32_TAG << TAG_SHIFT;
static constexpr u64 SHIFTED_IS_CELL_PATTERN = IS_CELL_PATTERN << TAG_SHIFT;
// Summary:
// To pack all the different value in to doubles we use the following schema:
// s = sign, e = exponent, m = mantissa
// The top part is the tag and the bottom the payload.
// 0bseeeeeeeeeeemmmm mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
// 0b0111111111111000 0... is the only real NaN
// 0b1111111111111xxx yyy... xxx = pointer type, yyy = pointer value
// 0b0111111111111xxx yyy... xxx = non-pointer type, yyy = value or 0 if just type
// Future expansion: We are not fully utilizing all the possible bit patterns
// yet, these choices were made to make it easy to implement and understand.
// We can for example drop the always 1 top bit of the mantissa expanding our
// options from 8 tags to 15 but since we currently only use 5 for both sign bits
// this is not needed.
class Value {
public:
enum class Type {
Empty,
Undefined,
Null,
Int32,
Double,
String,
Object,
Boolean,
Symbol,
Accessor,
BigInt,
};
enum class PreferredType {
Default,
String,
Number,
};
bool is_empty() const { return m_type == Type::Empty; }
bool is_undefined() const { return m_type == Type::Undefined; }
bool is_null() const { return m_type == Type::Null; }
bool is_number() const { return m_type == Type::Int32 || m_type == Type::Double; }
bool is_string() const { return m_type == Type::String; }
bool is_object() const { return m_type == Type::Object; }
bool is_boolean() const { return m_type == Type::Boolean; }
bool is_symbol() const { return m_type == Type::Symbol; }
bool is_accessor() const { return m_type == Type::Accessor; };
bool is_bigint() const { return m_type == Type::BigInt; };
bool is_nullish() const { return is_null() || is_undefined(); }
bool is_cell() const { return is_string() || is_accessor() || is_object() || is_bigint() || is_symbol(); }
bool is_empty() const { return m_value.tag == EMPTY_TAG; }
bool is_undefined() const { return m_value.tag == UNDEFINED_TAG; }
bool is_null() const { return m_value.tag == NULL_TAG; }
bool is_number() const { return is_double() || is_int32(); }
bool is_string() const { return m_value.tag == STRING_TAG; }
bool is_object() const { return m_value.tag == OBJECT_TAG; }
bool is_boolean() const { return m_value.tag == BOOLEAN_TAG; }
bool is_symbol() const { return m_value.tag == SYMBOL_TAG; }
bool is_accessor() const { return m_value.tag == ACCESSOR_TAG; };
bool is_bigint() const { return m_value.tag == BIGINT_TAG; };
bool is_nullish() const { return (m_value.tag & IS_NULLISH_EXTRACT_PATTERN) == IS_NULLISH_PATTERN; }
bool is_cell() const { return (m_value.tag & IS_CELL_PATTERN) == IS_CELL_PATTERN; }
ThrowCompletionOr<bool> is_array(GlobalObject&) const;
bool is_function() const;
bool is_constructor() const;
@ -69,84 +148,73 @@ public:
bool is_nan() const
{
if (type() == Type::Int32)
return false;
return is_number() && __builtin_isnan(as_double());
return m_value.encoded == CANON_NAN_BITS;
}
bool is_infinity() const
{
if (type() == Type::Int32)
return false;
return is_number() && __builtin_isinf(as_double());
static_assert(NEGATIVE_INFINITY_BITS == (0x1ULL << 63 | POSITIVE_INFINITY_BITS));
return (0x1ULL << 63 | m_value.encoded) == NEGATIVE_INFINITY_BITS;
}
bool is_positive_infinity() const
{
if (type() == Type::Int32)
return false;
return is_number() && __builtin_isinf_sign(as_double()) > 0;
return m_value.encoded == POSITIVE_INFINITY_BITS;
}
bool is_negative_infinity() const
{
if (type() == Type::Int32)
return false;
return is_number() && __builtin_isinf_sign(as_double()) < 0;
return m_value.encoded == NEGATIVE_INFINITY_BITS;
}
bool is_positive_zero() const
{
if (type() == Type::Int32)
return as_i32() == 0;
return is_number() && bit_cast<u64>(as_double()) == 0;
return m_value.encoded == 0 || (is_int32() && as_i32() == 0);
}
bool is_negative_zero() const
{
if (type() == Type::Int32)
return false;
return is_number() && bit_cast<u64>(as_double()) == NEGATIVE_ZERO_BITS;
return m_value.encoded == NEGATIVE_ZERO_BITS;
}
bool is_integral_number() const
{
if (type() == Type::Int32)
if (is_int32())
return true;
return is_finite_number() && trunc(as_double()) == as_double();
}
bool is_finite_number() const
{
if (type() == Type::Int32)
return true;
if (!is_number())
return false;
auto number = as_double();
return !__builtin_isnan(number) && !__builtin_isinf(number);
if (is_int32())
return true;
return !is_nan() && !is_infinity();
}
Value()
: m_type(Type::Empty)
: Value(EMPTY_TAG << TAG_SHIFT, (u64)0)
{
}
template<typename T>
requires(IsSameIgnoringCV<T, bool>) explicit Value(T value)
: m_type(Type::Boolean)
: Value(BOOLEAN_TAG << TAG_SHIFT, (u64)value)
{
m_value.as_bool = value;
}
explicit Value(double value)
{
bool is_negative_zero = bit_cast<u64>(value) == NEGATIVE_ZERO_BITS;
if (value >= NumericLimits<i32>::min() && value <= NumericLimits<i32>::max() && trunc(value) == value && !is_negative_zero) {
m_type = Type::Int32;
m_value.as_i32 = static_cast<i32>(value);
VERIFY(!(SHIFTED_INT32_TAG & (static_cast<i32>(value) & 0xFFFFFFFFul)));
m_value.encoded = SHIFTED_INT32_TAG | (static_cast<i32>(value) & 0xFFFFFFFFul);
} else {
m_type = Type::Double;
m_value.as_double = value;
if (isnan(value)) [[unlikely]]
m_value.encoded = CANON_NAN_BITS;
else
m_value.as_double = value;
}
}
@ -159,143 +227,130 @@ public:
{
if (value > NumericLimits<i32>::max()) {
m_value.as_double = static_cast<double>(value);
m_type = Type::Double;
} else {
m_value.as_i32 = static_cast<i32>(value);
m_type = Type::Int32;
VERIFY(!(SHIFTED_INT32_TAG & (static_cast<i32>(value) & 0xFFFFFFFFul)));
m_value.encoded = SHIFTED_INT32_TAG | (static_cast<i32>(value) & 0xFFFFFFFFul);
}
}
explicit Value(unsigned value)
{
if (value > NumericLimits<i32>::max()) {
m_value.as_double = static_cast<double>(value);
} else {
VERIFY(!(SHIFTED_INT32_TAG & (static_cast<i32>(value) & 0xFFFFFFFFul)));
m_value.encoded = SHIFTED_INT32_TAG | (static_cast<i32>(value) & 0xFFFFFFFFul);
}
}
explicit Value(i32 value)
: m_type(Type::Int32)
: Value(SHIFTED_INT32_TAG, (u32)value)
{
m_value.as_i32 = value;
}
Value(Object const* object)
: m_type(object ? Type::Object : Type::Null)
: Value(object ? (OBJECT_TAG << TAG_SHIFT) : (NULL_TAG << TAG_SHIFT), reinterpret_cast<void const*>(object))
{
m_value.as_object = const_cast<Object*>(object);
}
Value(PrimitiveString const* string)
: m_type(Type::String)
: Value(STRING_TAG << TAG_SHIFT, reinterpret_cast<void const*>(string))
{
m_value.as_string = const_cast<PrimitiveString*>(string);
}
Value(Symbol const* symbol)
: m_type(Type::Symbol)
: Value(SYMBOL_TAG << TAG_SHIFT, reinterpret_cast<void const*>(symbol))
{
m_value.as_symbol = const_cast<Symbol*>(symbol);
}
Value(Accessor const* accessor)
: m_type(Type::Accessor)
: Value(ACCESSOR_TAG << TAG_SHIFT, reinterpret_cast<void const*>(accessor))
{
m_value.as_accessor = const_cast<Accessor*>(accessor);
}
Value(BigInt const* bigint)
: m_type(Type::BigInt)
{
m_value.as_bigint = const_cast<BigInt*>(bigint);
}
explicit Value(Type type)
: m_type(type)
: Value(BIGINT_TAG << TAG_SHIFT, reinterpret_cast<void const*>(bigint))
{
}
Type type() const { return m_type; }
double as_double() const
{
VERIFY(is_number());
if (m_type == Type::Int32)
return m_value.as_i32;
if (is_int32())
return static_cast<i32>(m_value.encoded);
return m_value.as_double;
}
bool as_bool() const
{
VERIFY(type() == Type::Boolean);
return m_value.as_bool;
VERIFY(is_boolean());
return static_cast<bool>(m_value.encoded & 0x1);
}
Object& as_object()
{
VERIFY(type() == Type::Object);
return *m_value.as_object;
VERIFY(is_object());
return *extract_pointer<Object>();
}
Object const& as_object() const
{
VERIFY(type() == Type::Object);
return *m_value.as_object;
VERIFY(is_object());
return *extract_pointer<Object>();
}
PrimitiveString& as_string()
{
VERIFY(is_string());
return *m_value.as_string;
return *extract_pointer<PrimitiveString>();
}
PrimitiveString const& as_string() const
{
VERIFY(is_string());
return *m_value.as_string;
return *extract_pointer<PrimitiveString>();
}
Symbol& as_symbol()
{
VERIFY(is_symbol());
return *m_value.as_symbol;
return *extract_pointer<Symbol>();
}
Symbol const& as_symbol() const
{
VERIFY(is_symbol());
return *m_value.as_symbol;
return *extract_pointer<Symbol>();
}
Cell& as_cell()
{
VERIFY(is_cell());
return *m_value.as_cell;
return *extract_pointer<Cell>();
}
Accessor& as_accessor()
{
VERIFY(is_accessor());
return *m_value.as_accessor;
return *extract_pointer<Accessor>();
}
BigInt const& as_bigint() const
{
VERIFY(is_bigint());
return *extract_pointer<BigInt>();
}
BigInt& as_bigint()
{
VERIFY(is_bigint());
return *m_value.as_bigint;
return *extract_pointer<BigInt>();
}
Array& as_array();
FunctionObject& as_function();
FunctionObject const& as_function() const;
// FIXME: These two conversions are wrong for JS, and seem likely to be footguns
i32 as_i32() const
{
if (m_type == Type::Int32)
return m_value.as_i32;
return static_cast<i32>(as_double());
}
u32 as_u32() const
{
if (m_type == Type::Int32 && m_value.as_i32 >= 0)
return m_value.as_i32;
VERIFY(as_double() >= 0);
return (u32)min(as_double(), (double)NumericLimits<u32>::max());
}
u64 encoded() const { return m_value.encoded; }
ThrowCompletionOr<String> to_string(GlobalObject&) const;
@ -343,35 +398,86 @@ public:
[[nodiscard]] ALWAYS_INLINE ThrowCompletionOr<Value> invoke(GlobalObject& global_object, PropertyKey const& property_key, Args... args);
private:
Type m_type { Type::Empty };
Value(u64 tag, u64 val)
{
VERIFY((tag & ~TAG_EXTRACTION) == 0);
VERIFY(!(tag & val));
m_value.encoded = tag | val;
}
template<typename PointerType>
Value(u64 tag, PointerType const* ptr)
{
VERIFY((tag & ~TAG_EXTRACTION) == 0);
VERIFY((tag & TAG_EXTRACTION) != 0);
// Cell tag bit must be set or this is a nullptr
VERIFY((tag & 0x8000000000000000ul) == 0x8000000000000000ul || !ptr);
if constexpr (sizeof(PointerType*) < sizeof(u64)) {
m_value.encoded = tag | reinterpret_cast<u32>(ptr);
} else {
VERIFY(!(reinterpret_cast<u64>(ptr) & TAG_EXTRACTION));
m_value.encoded = tag | reinterpret_cast<u64>(ptr);
}
}
// A double is any Value which does not have the full exponent and top mantissa bit set or has
// exactly only those bits set.
bool is_double() const { return (m_value.encoded & CANON_NAN_BITS) != CANON_NAN_BITS || (m_value.encoded == CANON_NAN_BITS); }
bool is_int32() const { return m_value.tag == INT32_TAG; }
i32 as_i32() const
{
VERIFY(is_int32());
return static_cast<i32>(m_value.encoded & 0xFFFFFFFF);
}
template<typename PointerType>
PointerType* extract_pointer() const
{
VERIFY(is_cell());
// For 32-bit system the pointer fully fits so we can just return it directly.
if constexpr (sizeof(PointerType*) < sizeof(u64))
return reinterpret_cast<PointerType*>(static_cast<u32>(m_value.encoded & 0xffffffff));
// For x86_64 the top 16 bits should be sign extending the "real" top bit (47th).
// So first shift the top 16 bits away then using the right shift it sign extends the top 16 bits.
u64 ptr_val = (u64)(((i64)(m_value.encoded << 16)) >> 16);
return reinterpret_cast<PointerType*>(ptr_val);
}
[[nodiscard]] ThrowCompletionOr<Value> invoke_internal(GlobalObject& global_object, PropertyKey const&, Optional<MarkedVector<Value>> arguments);
ThrowCompletionOr<i32> to_i32_slow_case(GlobalObject&) const;
union {
bool as_bool;
i32 as_i32;
double as_double;
PrimitiveString* as_string;
Symbol* as_symbol;
Object* as_object;
Cell* as_cell;
Accessor* as_accessor;
BigInt* as_bigint;
struct {
u64 payload : 48;
u64 tag : 16;
};
u64 encoded;
} m_value { .encoded = 0 };
friend Value js_undefined();
friend Value js_null();
friend ThrowCompletionOr<Value> greater_than(GlobalObject&, Value lhs, Value rhs);
friend ThrowCompletionOr<Value> greater_than_equals(GlobalObject&, Value lhs, Value rhs);
friend ThrowCompletionOr<Value> less_than(GlobalObject&, Value lhs, Value rhs);
friend ThrowCompletionOr<Value> less_than_equals(GlobalObject&, Value lhs, Value rhs);
friend ThrowCompletionOr<Value> add(GlobalObject&, Value lhs, Value rhs);
friend bool same_value_non_numeric(Value lhs, Value rhs);
};
inline Value js_undefined()
{
return Value(Value::Type::Undefined);
return Value(UNDEFINED_TAG << TAG_SHIFT, (u64)0);
}
inline Value js_null()
{
return Value(Value::Type::Null);
return Value(NULL_TAG << TAG_SHIFT, (u64)0);
}
inline Value js_nan()
@ -433,6 +539,8 @@ inline bool Value::operator==(Value const& value) const { return same_value(*thi
namespace AK {
static_assert(sizeof(JS::Value) == sizeof(double));
template<>
class Optional<JS::Value> {
template<typename U>

View File

@ -74,7 +74,7 @@ ErrorOr<ByteBuffer> get_buffer_source_copy(JS::Object const& buffer_source)
// 9. For i in the range offset to offset + length 1, inclusive, set bytes[i offset] to ! GetValueFromBuffer(esArrayBuffer, i, Uint8, true, Unordered).
for (u64 i = offset; i <= offset + length - 1; ++i) {
auto value = es_array_buffer->get_value<u8>(i, true, JS::ArrayBuffer::Unordered);
bytes[i - offset] = (u8)value.as_u32();
bytes[i - offset] = static_cast<u8>(value.as_double());
}
// 10. Return bytes.