diff --git a/Userland/Libraries/LibJS/Runtime/BigIntConstructor.cpp b/Userland/Libraries/LibJS/Runtime/BigIntConstructor.cpp index 5eec127b63..20c767b8b3 100644 --- a/Userland/Libraries/LibJS/Runtime/BigIntConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/BigIntConstructor.cpp @@ -57,7 +57,7 @@ BigIntConstructor::~BigIntConstructor() Value BigIntConstructor::call() { - auto primitive = vm().argument(0).to_primitive(Value::PreferredType::Number); + auto primitive = vm().argument(0).to_primitive(global_object(), Value::PreferredType::Number); if (vm().exception()) return {}; if (primitive.is_number()) { diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 63a26e7333..1d4157855b 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -156,6 +156,7 @@ M(ThisHasNotBeenInitialized, "|this| has not been initialized") \ M(ThisIsAlreadyInitialized, "|this| is already initialized") \ M(ToObjectNullOrUndefined, "ToObject on null or undefined") \ + M(ToPrimitiveReturnedObject, "Can't convert {} to primitive with hint \"{}\", its @@toPrimitive method returned an object") \ M(TypedArrayInvalidBufferLength, "Invalid buffer length for {}: must be a multiple of {}, got {}") \ M(TypedArrayInvalidByteOffset, "Invalid byte offset for {}: must be a multiple of {}, got {}") \ M(TypedArrayOutOfRangeByteOffset, "Typed array byte offset {} is out of range for buffer with length {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/Value.cpp b/Userland/Libraries/LibJS/Runtime/Value.cpp index ff51da823c..34d96e2a29 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.cpp +++ b/Userland/Libraries/LibJS/Runtime/Value.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling - * Copyright (c) 2020, Linus Groh + * Copyright (c) 2020-2021, Linus Groh * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -301,7 +301,7 @@ String Value::to_string(GlobalObject& global_object, bool legacy_null_to_empty_s case Type::BigInt: return m_value.as_bigint->big_integer().to_base10(); case Type::Object: { - auto primitive_value = to_primitive(PreferredType::String); + auto primitive_value = to_primitive(global_object, PreferredType::String); if (global_object.vm().exception()) return {}; return primitive_value.to_string(global_object); @@ -336,10 +336,35 @@ bool Value::to_boolean() const } } -Value Value::to_primitive(PreferredType preferred_type) const +Value Value::to_primitive(GlobalObject& global_object, PreferredType preferred_type) const { + auto get_hint_for_preferred_type = [&]() -> String { + switch (preferred_type) { + case PreferredType::Default: + return "default"; + case PreferredType::String: + return "string"; + case PreferredType::Number: + return "number"; + default: + VERIFY_NOT_REACHED(); + } + }; if (is_object()) { - // FIXME: Also support @@toPrimitive + auto& vm = global_object.vm(); + auto to_primitive_method = get_method(global_object, *this, vm.well_known_symbol_to_primitive()); + if (vm.exception()) + return {}; + if (to_primitive_method) { + auto hint = get_hint_for_preferred_type(); + auto result = vm.call(*to_primitive_method, *this, js_string(vm, hint)); + if (vm.exception()) + return {}; + if (!result.is_object()) + return result; + vm.throw_exception(global_object, ErrorType::ToPrimitiveReturnedObject, to_string_without_side_effects(), hint); + return {}; + } if (preferred_type == PreferredType::Default) preferred_type = PreferredType::Number; return as_object().ordinary_to_primitive(preferred_type); @@ -374,7 +399,7 @@ Object* Value::to_object(GlobalObject& global_object) const Value Value::to_numeric(GlobalObject& global_object) const { - auto primitive = to_primitive(Value::PreferredType::Number); + auto primitive = to_primitive(global_object, Value::PreferredType::Number); if (global_object.vm().exception()) return {}; if (primitive.is_bigint()) @@ -414,7 +439,7 @@ Value Value::to_number(GlobalObject& global_object) const global_object.vm().throw_exception(global_object, ErrorType::Convert, "BigInt", "number"); return {}; case Type::Object: { - auto primitive = to_primitive(PreferredType::Number); + auto primitive = to_primitive(global_object, PreferredType::Number); if (global_object.vm().exception()) return {}; return primitive.to_number(global_object); @@ -427,7 +452,7 @@ Value Value::to_number(GlobalObject& global_object) const BigInt* Value::to_bigint(GlobalObject& global_object) const { auto& vm = global_object.vm(); - auto primitive = to_primitive(PreferredType::Number); + auto primitive = to_primitive(global_object, PreferredType::Number); if (vm.exception()) return nullptr; switch (primitive.type()) { @@ -789,10 +814,10 @@ Value unsigned_right_shift(GlobalObject& global_object, Value lhs, Value rhs) Value add(GlobalObject& global_object, Value lhs, Value rhs) { - auto lhs_primitive = lhs.to_primitive(); + auto lhs_primitive = lhs.to_primitive(global_object); if (global_object.vm().exception()) return {}; - auto rhs_primitive = rhs.to_primitive(); + auto rhs_primitive = rhs.to_primitive(global_object); if (global_object.vm().exception()) return {}; @@ -1094,11 +1119,19 @@ bool abstract_eq(GlobalObject& global_object, Value lhs, Value rhs) if (rhs.is_boolean()) return abstract_eq(global_object, lhs, rhs.to_number(global_object.global_object())); - if ((lhs.is_string() || lhs.is_number() || lhs.is_bigint() || lhs.is_symbol()) && rhs.is_object()) - return abstract_eq(global_object, lhs, rhs.to_primitive()); + if ((lhs.is_string() || lhs.is_number() || lhs.is_bigint() || lhs.is_symbol()) && rhs.is_object()) { + auto rhs_primitive = rhs.to_primitive(global_object); + if (global_object.vm().exception()) + return false; + return abstract_eq(global_object, lhs, rhs_primitive); + } - if (lhs.is_object() && (rhs.is_string() || rhs.is_number() || lhs.is_bigint() || rhs.is_symbol())) - return abstract_eq(global_object, lhs.to_primitive(), rhs); + if (lhs.is_object() && (rhs.is_string() || rhs.is_number() || lhs.is_bigint() || rhs.is_symbol())) { + auto lhs_primitive = lhs.to_primitive(global_object); + if (global_object.vm().exception()) + return false; + return abstract_eq(global_object, lhs_primitive, rhs); + } if ((lhs.is_bigint() && rhs.is_number()) || (lhs.is_number() && rhs.is_bigint())) { if (lhs.is_nan() || lhs.is_infinity() || rhs.is_nan() || rhs.is_infinity()) @@ -1120,17 +1153,17 @@ TriState abstract_relation(GlobalObject& global_object, bool left_first, Value l Value y_primitive; if (left_first) { - x_primitive = lhs.to_primitive(Value::PreferredType::Number); + x_primitive = lhs.to_primitive(global_object, Value::PreferredType::Number); if (global_object.vm().exception()) return {}; - y_primitive = rhs.to_primitive(Value::PreferredType::Number); + y_primitive = rhs.to_primitive(global_object, Value::PreferredType::Number); if (global_object.vm().exception()) return {}; } else { - y_primitive = lhs.to_primitive(Value::PreferredType::Number); + y_primitive = lhs.to_primitive(global_object, Value::PreferredType::Number); if (global_object.vm().exception()) return {}; - x_primitive = rhs.to_primitive(Value::PreferredType::Number); + x_primitive = rhs.to_primitive(global_object, Value::PreferredType::Number); if (global_object.vm().exception()) return {}; } diff --git a/Userland/Libraries/LibJS/Runtime/Value.h b/Userland/Libraries/LibJS/Runtime/Value.h index 3914232306..1261bbfa65 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.h +++ b/Userland/Libraries/LibJS/Runtime/Value.h @@ -248,7 +248,7 @@ public: String to_string(GlobalObject&, bool legacy_null_to_empty_string = false) const; PrimitiveString* to_primitive_string(GlobalObject&); - Value to_primitive(PreferredType preferred_type = PreferredType::Default) const; + Value to_primitive(GlobalObject&, PreferredType preferred_type = PreferredType::Default) const; Object* to_object(GlobalObject&) const; Value to_numeric(GlobalObject&) const; Value to_number(GlobalObject&) const; diff --git a/Userland/Libraries/LibJS/Tests/custom-@@toPrimitive.js b/Userland/Libraries/LibJS/Tests/custom-@@toPrimitive.js new file mode 100644 index 0000000000..2024068ee8 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/custom-@@toPrimitive.js @@ -0,0 +1,20 @@ +test("basic functionality", () => { + const o = { + [Symbol.toPrimitive]: hint => { + lastHint = hint; + }, + }; + let lastHint; + + // Calls ToPrimitive abstract operation with 'string' hint + String(o); + expect(lastHint).toBe("string"); + + // Calls ToPrimitive abstract operation with 'number' hint + +o; + expect(lastHint).toBe("number"); + + // Calls ToPrimitive abstract operation with 'default' hint + "" + o; + expect(lastHint).toBe("default"); +});