diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 0e437c961..885bf81d5 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -159,12 +159,17 @@ fn has_at(interp: &mut Interp, o: ObjId, k: f64) -> JsResult { /// The estimated bytes a new element adds. const ELEMENT_BYTES: usize = PROP_BYTES + 32 + 2 * 10; -/// `o[k] = v` as the array methods assign: a failure throws. `v` stays rooted while heap is -/// reserved for it, since the caller may hold it nowhere else. -fn put_at(interp: &mut Interp, o: ObjId, k: f64, v: Value) -> JsResult<()> { +/// `o[k] = v` as the array methods assign: a failure throws. When `o` has no own property `k`, `v` +/// stays rooted while heap is reserved for the new element, since the caller may hold it nowhere +/// else; overwriting an own property reserves nothing. +pub(crate) fn put_at(interp: &mut Interp, o: ObjId, k: f64, v: Value) -> JsResult<()> { + let k = key_of(k); + if interp.heap.object(o).is_some_and(|obj| obj.props.contains(&k)) { + return interp.put(o, k, v, true); + } interp.rooted(std::slice::from_ref(&v), |i| { i.reserve_heap(ELEMENT_BYTES)?; - i.put(o, key_of(k), v.clone(), true) + i.put(o, k.clone(), v.clone(), true) }) } @@ -357,11 +362,12 @@ fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> J fn object_to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { let o = interp.to_object(&this)?; - let f = interp.get_from(o, &key("toString"), &this)?; + let receiver = Value::Object(o); + let f = interp.get_from(o, &key("toString"), &receiver)?; if !interp.is_callable(&f) { return Err(interp.type_error("toString is not a function")); } - interp.call(&f, &this, &[]) + interp.call(&f, &receiver, &[]) } fn object_value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index 7261054b7..f3d2cb7f6 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -369,6 +369,38 @@ fn a_heap_limit_smaller_than_the_built_ins_still_installs_every_built_in() { ); } +#[test] +fn to_locale_string_calls_to_string_on_the_boxed_value() { + assert_eq!( + string( + "Object.defineProperty(Object.getPrototypeOf(Object(5)), 'toString', {value: function () { 'use strict'; return typeof this; }}); \ + Object.prototype.toLocaleString.call(5)" + ), + "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 + // succeeds, and a write that adds an element stops with the heap limit. + use crate::heap::key; + use crate::interp::Abrupt; + use crate::value::Value; + let mut realm = small(Limits::default()); + assert_eq!(realm.run("var a = [1, 2]; 0"), Ok(JsValue::Number(0.0))); + let interp = realm.interp(); + let global = interp.global; + let a = match interp.get_from(global, &key("a"), &Value::Object(global)) { + Ok(Value::Object(a)) => a, + other => panic!("{other:?}"), + }; + interp.collect_garbage(); + interp.limits.heap_bytes = interp.heap.bytes; + assert!(crate::builtins::put_at(interp, a, 0.0, Value::Number(5.0)).is_ok()); + assert!(matches!(crate::builtins::put_at(interp, a, 2.0, Value::Number(5.0)), Err(Abrupt::Limit(LimitKind::Heap)))); +} + #[test] fn a_realm_whose_built_ins_failed_to_install_runs_nothing() { let mut realm = Realm::new();