JavaScript: toLocaleString writes the primitive value of Number, String and Boolean objects

`[1, 'x', true].toLocaleString()` gave `[object Number],[object String],[object Boolean]`
because the wrappers' prototypes have no `toString` on this branch and the lookup reached
Object.prototype.toString. Object.prototype.toLocaleString now returns the string form of
the object's primitive value when `toString` is not found on the object or on its chain up
to its standard prototype. A `toString` on the object or that prototype is still called.
This commit is contained in:
dotCooCoo
2026-11-08 15:43:09 -08:00
parent ba5c57790f
commit 4afee338cf
3 changed files with 36 additions and 1 deletions
+20
View File
@@ -360,8 +360,28 @@ fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> J
Ok(Value::str(&format!("[object {class}]")))
}
/// The string form of a Number, String or Boolean object's [[PrimitiveValue]] (sections 8.6.2 and
/// 9.9), when `toString` is not found on the object or on its prototype chain up to and including
/// its standard prototype. Returns `None` otherwise, and for any other object.
fn boxed_primitive_string(interp: &mut Interp, o: ObjId) -> JsResult<Option<Value>> {
let (primitive, proto) = match interp.heap.object(o).map(|x| &x.class) {
Some(Class::Number(n)) => (Value::Number(*n), interp.intrinsics.number_proto),
Some(Class::String(s)) => (Value::String(s.clone()), interp.intrinsics.string_proto),
Some(Class::Boolean(b)) => (Value::Bool(*b), interp.intrinsics.boolean_proto),
_ => return Ok(None),
};
if !json::reaches_prototype(interp, o, proto, "toString")? {
return Ok(None);
}
Ok(Some(Value::String(interp.primitive_to_str(&primitive))))
}
/// `Object.prototype.toLocaleString` (section 15.2.4.3).
fn object_to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let o = interp.to_object(&this)?;
if let Some(s) = boxed_primitive_string(interp, o)? {
return Ok(s);
}
let receiver = Value::Object(o);
let f = interp.get_from(o, &key("toString"), &receiver)?;
if !interp.is_callable(&f) {
+1 -1
View File
@@ -574,7 +574,7 @@ fn unwrap_primitive(interp: &mut Interp, v: &Value, as_string: bool) -> JsResult
/// Whether looking up `name` from `o` reaches the object `proto` on the prototype chain before
/// any object on the way, `proto` included, has its own property `name`.
fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult<bool> {
pub(super) fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult<bool> {
let k = key(name);
let mut cur = Some(o);
while let Some(id) = cur {
+15
View File
@@ -380,6 +380,21 @@ fn to_locale_string_calls_to_string_on_the_boxed_value() {
);
}
#[test]
fn to_locale_string_writes_the_primitive_of_a_boxed_value() {
assert_eq!(string("[1, 'x', true, false, 1.5].toLocaleString()"), "1,x,true,false,1.5");
assert_eq!(string("Object.prototype.toLocaleString.call(5)"), "5");
assert_eq!(string("Object.prototype.toLocaleString.call(Object('s'))"), "s");
// Object.prototype.toString comes after the standard prototypes on the chain.
assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; [1, 'x', true].toLocaleString()"), "1,x,true");
// A toString on the object or on its standard prototype is called.
assert_eq!(string("var n = Object(1); n.toString = function () { return 'own'; }; [n].toLocaleString()"), "own");
assert_eq!(string("Object.getPrototypeOf(true).toString = function () { return 'p'; }; [true].toLocaleString()"), "p");
assert_eq!(string("Object.getPrototypeOf(1).toString = Object.prototype.toString; [1].toLocaleString()"), "[object Number]");
// Other objects still reach Object.prototype.toString.
assert_eq!(string("Object.prototype.toLocaleString.call({})"), "[object Object]");
}
#[test]
fn array_methods_overwriting_elements_need_no_heap_for_new_ones() {
// With the heap exactly at its limit, an array method's write over an existing element