Files
vectorcraft/crates/js/src/builtins/array.rs
T
dotCooCoo 46efc4a651 Scripts: a String object counts its rope's copy when a character is read
- wrapping a string no longer flattens it, so Object(s).length and new String(s) on a large concatenated string make no copy and cannot collect while a call's operands are off the stack
- reading a String object's character by index, `in` and hasOwnProperty count the flattened copy at the next limit check, and the run ends before the copy when the fuel for it is not left
- unary plus on an object whose valueOf returns a concatenated string, and a property key from toString, keep that string rooted while its copy is reserved
- the unshift comment cites step 6 of section 15.4.4.13
2026-11-11 22:39:04 -08:00

862 lines
32 KiB
Rust

//! `Array` (ECMA-262 5.1 section 15.4): the constructor, `Array.isArray` and every method of
//! `Array.prototype`. The methods are generic: they read `length` and the elements of any object.
//!
//! A method that walks a range of indices visits only the indices that can hold an element when
//! the range is much longer than the object has properties (a sparse array such as
//! `new Array(4294967295)`), so a long empty range costs nothing and no index is allocated for
//! it. The list of indices is made again whenever a script adds an index property to any object
//! while the method runs. Every index visited takes a unit of fuel.
use super::{
Builder, ELEMENT_BYTES, arg, constructor, create_at, delete_at, get_at, has_at, length_of, method, native, new_array, object_to_string, put_at,
require_callable, to_integer,
};
use crate::heap::{Class, NativeKind, ObjId};
use crate::interp::{Abrupt, Interp, JsResult, strict_eq};
use crate::string_builtins::flat_charged;
use crate::value::{Str, Value, to_uint32};
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
let proto = interp.intrinsics.array_proto;
let array = native(interp, "Array", array_ctor, 1, NativeKind::Plain, true)?;
constructor(interp, "Array", array, proto)?;
method(interp, array, "isArray", is_array, 1)?;
let methods: [(&'static str, crate::heap::NativeFn, u32); 21] = [
("toString", to_string, 0),
("toLocaleString", to_locale_string, 0),
("concat", concat, 1),
("join", join, 1),
("pop", pop, 0),
("push", push, 1),
("reverse", reverse, 0),
("shift", shift, 0),
("slice", slice, 2),
("sort", sort, 1),
("splice", splice, 2),
("unshift", unshift, 1),
("indexOf", index_of, 1),
("lastIndexOf", last_index_of, 1),
("every", every, 1),
("some", some, 1),
("forEach", for_each, 1),
("map", map, 1),
("filter", filter, 1),
("reduce", reduce, 1),
("reduceRight", reduce_right, 1),
];
for (name, f, length) in methods {
method(interp, proto, name, f, length)?;
}
Ok(())
}
// ---- visiting indices ----
/// Ranges at most this long are visited index by index.
const DENSE_SPAN: u64 = 4096;
/// How an index an object holds names an index to visit.
#[derive(Clone, Copy)]
enum Map {
/// The index `p` asks for a visit of `p - shift`.
Shift(i64),
/// The index `p` asks for a visit of `p` and of `mirror - p`.
Mirror(i64),
}
/// The indices of a range, in order, that an operation must visit.
struct Indices {
obj: ObjId,
/// The range left to visit, `lo` included and `hi` not.
lo: u64,
hi: u64,
ascending: bool,
maps: Vec<Map>,
/// The indices left when the range is visited sparsely.
sparse: Option<Sparse>,
}
struct Sparse {
list: Vec<u64>,
pos: usize,
/// [`crate::heap::Heap::index_epoch`] when the list was made.
epoch: u64,
}
/// The canonical numeric property name `k` stands for, up to 15 digits.
fn numeric_index(k: &[u16]) -> Option<u64> {
if k.is_empty() || k.len() > 15 || (k.len() > 1 && k.first() == Some(&u16::from(b'0'))) {
return None;
}
let mut n = 0u64;
for &u in k {
let d = u.checked_sub(u16::from(b'0')).filter(|d| *d <= 9)?;
n = n * 10 + u64::from(d);
}
Some(n)
}
/// The objects of the prototype chain from `obj`, with how many properties (and String object
/// indices) each holds.
fn chain(interp: &Interp, obj: ObjId) -> Vec<(ObjId, usize)> {
let mut out = Vec::new();
let mut cur = Some(obj);
while let Some(id) = cur {
let Some(o) = interp.heap.object(id) else { break };
let string_len = if let Class::String(s) = &o.class { s.len() } else { 0 };
out.push((id, o.props.len().saturating_add(string_len)));
cur = o.proto;
}
out
}
impl Indices {
fn new(interp: &mut Interp, obj: ObjId, lo: u64, hi: u64, ascending: bool, maps: Vec<Map>) -> JsResult<Self> {
let mut it = Indices { obj, lo, hi, ascending, maps, sparse: None };
if hi.saturating_sub(lo) > DENSE_SPAN {
let present: usize = chain(interp, obj).iter().map(|(_, n)| *n).fold(0, usize::saturating_add);
if hi - lo > (present as u64).saturating_mul(8) {
it.sparse = Some(it.gather(interp)?);
}
}
Ok(it)
}
fn up(interp: &mut Interp, obj: ObjId, lo: u64, hi: u64) -> JsResult<Self> {
Self::new(interp, obj, lo, hi, true, vec![Map::Shift(0)])
}
fn down(interp: &mut Interp, obj: ObjId, lo: u64, hi: u64) -> JsResult<Self> {
Self::new(interp, obj, lo, hi, false, vec![Map::Shift(0)])
}
/// Lists the indices left to visit from the properties of the object and its prototypes.
/// Each property takes a unit of fuel.
fn gather(&self, interp: &mut Interp) -> JsResult<Sparse> {
let epoch = interp.heap.index_epoch;
let mut list: Vec<u64> = Vec::new();
for (id, n) in chain(interp, self.obj) {
for _ in 0..n {
interp.use_fuel()?;
}
interp.reserve_heap(n.saturating_mul(8).saturating_mul(self.maps.len() + 1))?;
let Some(o) = interp.heap.object(id) else { continue };
let string_len = if let Class::String(s) = &o.class { s.len() } else { 0 };
let present = (0..string_len as u64).chain(o.props.iter().filter_map(|(k, _)| numeric_index(k)));
for p in present {
for map in &self.maps {
let p = p as i64;
let ks = match *map {
Map::Shift(s) => [Some(p - s), None],
Map::Mirror(m) => [Some(p), Some(m - p)],
};
for k in ks.into_iter().flatten() {
if k >= 0 && (k as u64) >= self.lo && (k as u64) < self.hi {
list.push(k as u64);
}
}
}
}
}
if self.ascending {
list.sort_unstable();
} else {
list.sort_unstable_by(|a, b| b.cmp(a));
}
list.dedup();
Ok(Sparse { list, pos: 0, epoch })
}
/// The next index to visit; it takes a unit of fuel.
fn next(&mut self, interp: &mut Interp) -> JsResult<Option<u64>> {
if self.lo >= self.hi {
return Ok(None);
}
if self.sparse.as_ref().is_some_and(|s| s.epoch != interp.heap.index_epoch) {
self.sparse = Some(self.gather(interp)?);
}
let k = match &mut self.sparse {
None if self.ascending => self.lo,
None => self.hi - 1,
Some(s) => {
let Some(&k) = s.list.get(s.pos) else {
self.lo = self.hi;
return Ok(None);
};
s.pos += 1;
k
}
};
if self.ascending {
self.lo = k + 1;
} else {
self.hi = k;
}
interp.use_fuel()?;
Ok(Some(k))
}
}
/// The element at `k` when the object or its prototypes have one.
fn element(interp: &mut Interp, o: ObjId, k: u64) -> JsResult<Option<Value>> {
let k = k as f64;
if !has_at(interp, o, k)? {
return Ok(None);
}
get_at(interp, o, k).map(Some)
}
/// ToObject of `this`, kept reachable while `f` runs.
fn with_this<T>(interp: &mut Interp, this: &Value, f: impl FnOnce(&mut Interp, ObjId) -> JsResult<T>) -> JsResult<T> {
let o = interp.to_object(this)?;
interp.rooted(&[Value::Object(o)], |interp| f(interp, o))
}
/// The start a relative index gives within `0..=len` (`slice` and `splice`).
fn relative(interp: &mut Interp, v: &Value, len: u32) -> JsResult<u64> {
let n = to_integer(interp, v)?;
let len = f64::from(len);
Ok(if n < 0.0 { (len + n).max(0.0) } else { n.min(len) } as u64)
}
fn number(k: u64) -> Value {
Value::Number(k as f64)
}
// ---- the constructor ----
/// `Array(…)` and `new Array(…)` (sections 15.4.1 and 15.4.2).
fn array_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
if let [Value::Number(n)] = args {
let len = to_uint32(*n);
if f64::from(len) != *n {
return Err(interp.throw(crate::interp::ErrorType::Range, "invalid array length"));
}
return new_array(interp, len).map(Value::Object);
}
let a = new_array(interp, 0)?;
interp.rooted(&[Value::Object(a)], |interp| {
for (i, v) in args.iter().enumerate() {
interp.use_fuel()?;
create_at(interp, a, i as f64, v.clone())?;
}
Ok(Value::Object(a))
})
}
/// `Array.isArray` (section 15.4.3.2).
fn is_array(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
let a = arg(args, 0).as_object().and_then(|o| interp.heap.object(o)).is_some_and(|o| matches!(o.class, Class::Array { .. }));
Ok(Value::Bool(a))
}
// ---- conversions ----
/// `Array.prototype.toString` (section 15.4.4.2).
fn to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let join = interp.get_from(o, &crate::heap::key("join"), &Value::Object(o))?;
if interp.is_callable(&join) {
return interp.call(&join, &Value::Object(o), &[]);
}
object_to_string(interp, Value::Object(o), &[], false)
})
}
/// Joins the elements `0..len` of `o`, each turned into a string by `item`, with `separator`
/// between them (`,` when it is `undefined`). `length` is read before the separator is converted,
/// and the separator is converted even when `length` is 0 (section 15.4.4.5 steps 2 to 6). A cycle
/// back to an object being joined joins as an empty string.
fn join_with(interp: &mut Interp, o: ObjId, separator: &Value, item: fn(&mut Interp, Value) -> JsResult<Option<Str>>) -> JsResult<Value> {
if interp.join_stack.contains(&o) {
return Ok(Value::str(""));
}
let len = length_of(interp, o)?;
let sep = match separator {
Value::Undefined => Str::from(","),
v => interp.to_str(v)?,
};
if len == 0 {
return Ok(Value::str(""));
}
// The separator stays rooted for the whole join, so its flattened copy stays counted.
interp.rooted(&[Value::String(sep.clone())], |interp| {
let flat = flat_charged(interp, &sep)?;
let sep = &flat[..];
// A result that cannot fit is refused before any element is read.
let least = (sep.len() as u64).saturating_mul(u64::from(len - 1));
if least > (interp.limits.heap_bytes / 4) as u64 {
return Err(Abrupt::Limit(crate::LimitKind::Heap));
}
interp.join_stack.push(o);
let r = join_elements(interp, o, len, sep, item);
interp.join_stack.pop();
r
})
}
fn join_elements(interp: &mut Interp, o: ObjId, len: u32, sep: &[u16], item: fn(&mut Interp, Value) -> JsResult<Option<Str>>) -> JsResult<Value> {
let mut out = Builder::new();
// Separators written so far: element `k` follows `k` of them.
let mut seps = 0u64;
let mut it = Indices::up(interp, o, 0, u64::from(len))?;
while let Some(k) = it.next(interp)? {
let v = get_at(interp, o, k as f64)?;
let Some(s) = item(interp, v)? else { continue };
out.push_repeated(interp, sep, k.saturating_sub(seps))?;
seps = k;
interp.rooted(&[Value::String(s.clone())], |interp| {
let units = flat_charged(interp, &s)?;
out.push_units(interp, &units)
})?;
}
out.push_repeated(interp, sep, u64::from(len - 1).saturating_sub(seps))?;
Ok(out.finish(interp))
}
/// An element as `join` writes it: nothing for `undefined` and `null`.
fn join_item(interp: &mut Interp, v: Value) -> JsResult<Option<Str>> {
if v.is_nullish() {
return Ok(None);
}
interp.to_str(&v).map(Some)
}
/// An element as `toLocaleString` writes it (section 15.4.4.3).
fn locale_item(interp: &mut Interp, v: Value) -> JsResult<Option<Str>> {
if v.is_nullish() {
return Ok(None);
}
let e = interp.to_object(&v)?;
let f = interp.get_from(e, &crate::heap::key("toLocaleString"), &Value::Object(e))?;
require_callable(interp, &f, "an element's toLocaleString")?;
let s = interp.call(&f, &Value::Object(e), &[])?;
interp.to_str(&s).map(Some)
}
/// `Array.prototype.join` (section 15.4.4.5).
fn join(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let separator = arg(args, 0);
with_this(interp, &this, |interp, o| join_with(interp, o, &separator, join_item))
}
/// `Array.prototype.toLocaleString` (section 15.4.4.3).
fn to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| join_with(interp, o, &Value::Undefined, locale_item))
}
// ---- methods that change the object ----
/// `Array.prototype.concat` (section 15.4.4.4).
fn concat(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let a = new_array(interp, 0)?;
interp.rooted(&[Value::Object(a)], |interp| {
let mut n = 0u64;
for e in std::iter::once(Value::Object(o)).chain(args.iter().cloned()) {
interp.use_fuel()?;
let spread = e.as_object().filter(|&id| interp.heap.object(id).is_some_and(|x| matches!(x.class, Class::Array { .. })));
let Some(e) = spread else {
create_at(interp, a, n as f64, e)?;
n += 1;
continue;
};
let len = u64::from(length_of(interp, e)?);
let mut it = Indices::up(interp, e, 0, len)?;
while let Some(k) = it.next(interp)? {
if let Some(v) = element(interp, e, k)? {
create_at(interp, a, (n + k) as f64, v)?;
}
}
n += len;
}
set_length(interp, a, n as f64)?;
Ok(Value::Object(a))
})
})
}
fn set_length(interp: &mut Interp, o: ObjId, n: f64) -> JsResult<()> {
interp.put(o, crate::heap::key("length"), Value::Number(n), true)
}
/// `Array.prototype.pop` (section 15.4.4.6).
fn pop(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = length_of(interp, o)?;
if len == 0 {
set_length(interp, o, 0.0)?;
return Ok(Value::Undefined);
}
let last = f64::from(len - 1);
let v = get_at(interp, o, last)?;
interp.rooted(std::slice::from_ref(&v), |interp| {
delete_at(interp, o, last)?;
set_length(interp, o, last)
})?;
Ok(v)
})
}
/// `Array.prototype.push` (section 15.4.4.7).
fn push(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let mut n = f64::from(length_of(interp, o)?);
for v in args {
interp.use_fuel()?;
put_at(interp, o, n, v.clone())?;
n += 1.0;
}
set_length(interp, o, n)?;
Ok(Value::Number(n))
})
}
/// `Array.prototype.reverse` (section 15.4.4.8).
fn reverse(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = u64::from(length_of(interp, o)?);
let middle = len / 2;
let mirror = len as i64 - 1;
let mut it = Indices::new(interp, o, 0, middle, true, vec![Map::Mirror(mirror)])?;
while let Some(lower) = it.next(interp)? {
let upper = len - lower - 1;
let lower_value = element(interp, o, lower)?;
let upper_value = interp.rooted(lower_value.as_slice(), |interp| element(interp, o, upper))?;
let mut roots: Vec<Value> = lower_value.iter().cloned().collect();
roots.extend(upper_value.iter().cloned());
interp.rooted(&roots, |interp| match (lower_value, upper_value) {
(Some(l), Some(u)) => {
put_at(interp, o, lower as f64, u)?;
put_at(interp, o, upper as f64, l)
}
(None, Some(u)) => {
put_at(interp, o, lower as f64, u)?;
delete_at(interp, o, upper as f64)
}
(Some(l), None) => {
delete_at(interp, o, lower as f64)?;
put_at(interp, o, upper as f64, l)
}
(None, None) => Ok(()),
})?;
}
Ok(Value::Object(o))
})
}
/// Moves the element at `from` to `to`, or deletes `to` when there is none at `from`.
fn move_element(interp: &mut Interp, o: ObjId, from: u64, to: u64) -> JsResult<()> {
match element(interp, o, from)? {
Some(v) => put_at(interp, o, to as f64, v),
None => delete_at(interp, o, to as f64),
}
}
/// `Array.prototype.shift` (section 15.4.4.9).
fn shift(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = u64::from(length_of(interp, o)?);
if len == 0 {
set_length(interp, o, 0.0)?;
return Ok(Value::Undefined);
}
let first = get_at(interp, o, 0.0)?;
interp.rooted(std::slice::from_ref(&first), |interp| {
let mut it = Indices::new(interp, o, 1, len, true, vec![Map::Shift(0), Map::Shift(-1)])?;
while let Some(k) = it.next(interp)? {
move_element(interp, o, k, k - 1)?;
}
delete_at(interp, o, (len - 1) as f64)?;
set_length(interp, o, (len - 1) as f64)
})?;
Ok(first)
})
}
/// `Array.prototype.unshift` (section 15.4.4.13).
fn unshift(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = u64::from(length_of(interp, o)?);
let count = args.len() as u64;
// With no arguments each element is still read and written back in place (step 6).
let maps = vec![Map::Shift(-1), Map::Shift(count as i64 - 1)];
let mut it = Indices::new(interp, o, 1, len + 1, false, maps)?;
while let Some(k) = it.next(interp)? {
move_element(interp, o, k - 1, k + count - 1)?;
}
for (j, v) in args.iter().enumerate() {
interp.use_fuel()?;
put_at(interp, o, j as f64, v.clone())?;
}
let n = (len + count) as f64;
set_length(interp, o, n)?;
Ok(Value::Number(n))
})
}
/// `Array.prototype.slice` (section 15.4.4.10).
fn slice(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = length_of(interp, o)?;
let start = relative(interp, &arg(args, 0), len)?;
let end = match arg(args, 1) {
Value::Undefined => u64::from(len),
v => relative(interp, &v, len)?,
};
let a = new_array(interp, 0)?;
interp.rooted(&[Value::Object(a)], |interp| {
let mut it = Indices::up(interp, o, start, end)?;
while let Some(k) = it.next(interp)? {
if let Some(v) = element(interp, o, k)? {
create_at(interp, a, (k - start) as f64, v)?;
}
}
set_length(interp, a, end.saturating_sub(start) as f64)?;
Ok(Value::Object(a))
})
})
}
/// `Array.prototype.splice` (section 15.4.4.12).
fn splice(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = length_of(interp, o)?;
let start = relative(interp, &arg(args, 0), len)?;
let len = u64::from(len);
let delete_count = match args.get(1) {
// Without a count, everything from the start is deleted, as later editions and
// every implementation do.
None if !args.is_empty() => len - start,
None => 0,
Some(v) => to_integer(interp, v)?.max(0.0).min((len - start) as f64) as u64,
};
let items = args.get(2..).unwrap_or_default();
let a = new_array(interp, 0)?;
interp.rooted(&[Value::Object(a)], |interp| {
let mut it = Indices::up(interp, o, start, start + delete_count)?;
while let Some(k) = it.next(interp)? {
if let Some(v) = element(interp, o, k)? {
create_at(interp, a, (k - start) as f64, v)?;
}
}
set_length(interp, a, delete_count as f64)?;
move_tail(interp, o, len, start, delete_count, items.len() as u64)?;
for (j, v) in items.iter().enumerate() {
interp.use_fuel()?;
put_at(interp, o, (start + j as u64) as f64, v.clone())?;
}
set_length(interp, o, (len - delete_count + items.len() as u64) as f64)?;
Ok(Value::Object(a))
})
})
}
/// Moves the elements after a spliced range so that `item_count` items replace `delete_count`
/// elements at `start` (section 15.4.4.12, steps 12 and 13).
fn move_tail(interp: &mut Interp, o: ObjId, len: u64, start: u64, delete_count: u64, item_count: u64) -> JsResult<()> {
let (dc, ic) = (delete_count as i64, item_count as i64);
if item_count < delete_count {
let mut it = Indices::new(interp, o, start, len - delete_count, true, vec![Map::Shift(dc), Map::Shift(ic)])?;
while let Some(k) = it.next(interp)? {
move_element(interp, o, k + delete_count, k + item_count)?;
}
let mut it = Indices::down(interp, o, len - delete_count + item_count, len)?;
while let Some(k) = it.next(interp)? {
delete_at(interp, o, k as f64)?;
}
} else if item_count > delete_count {
let mut it = Indices::new(interp, o, start + 1, len - delete_count + 1, false, vec![Map::Shift(dc - 1), Map::Shift(ic - 1)])?;
while let Some(k) = it.next(interp)? {
move_element(interp, o, k + delete_count - 1, k + item_count - 1)?;
}
}
Ok(())
}
// ---- sort ----
/// A stable merge sort whose comparison may fail. `greater(a, b)` says whether `a` sorts after
/// `b`; each comparison takes a unit of fuel.
fn merge_sort<T: Clone>(interp: &mut Interp, items: &mut Vec<T>, mut greater: impl FnMut(&mut Interp, &T, &T) -> JsResult<bool>) -> JsResult<()> {
let n = items.len();
let mut width = 1;
let mut buf: Vec<T> = Vec::with_capacity(n);
while width < n {
buf.clear();
let mut lo = 0;
while lo < n {
let mid = (lo + width).min(n);
let hi = (lo + 2 * width).min(n);
let (mut i, mut j) = (lo, mid);
while i < mid && j < hi {
let (Some(a), Some(b)) = (items.get(i), items.get(j)) else { break };
interp.use_fuel()?;
if greater(interp, a, b)? {
buf.push(b.clone());
j += 1;
} else {
buf.push(a.clone());
i += 1;
}
}
buf.extend(items.get(i..mid).unwrap_or_default().iter().cloned());
buf.extend(items.get(j..hi).unwrap_or_default().iter().cloned());
lo = hi;
}
std::mem::swap(items, &mut buf);
width *= 2;
}
Ok(())
}
/// `Array.prototype.sort` (section 15.4.4.11). The elements are read, sorted with `undefined`
/// after every other value, written back from index 0, and the indices after them are deleted.
fn sort(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let compare = arg(args, 0);
if !matches!(compare, Value::Undefined) {
require_callable(interp, &compare, "the comparison")?;
}
with_this(interp, &this, |interp, o| {
let len = u64::from(length_of(interp, o)?);
let mark = interp.temp_roots.len();
let r = sort_elements(interp, o, len, &compare);
interp.temp_roots.truncate(mark);
r?;
Ok(Value::Object(o))
})
}
fn sort_elements(interp: &mut Interp, o: ObjId, len: u64, compare: &Value) -> JsResult<()> {
let mut values = Vec::new();
let mut undefined = 0u64;
let mut it = Indices::up(interp, o, 0, len)?;
while let Some(k) = it.next(interp)? {
match element(interp, o, k)? {
Some(Value::Undefined) => undefined += 1,
Some(v) => {
interp.temp_roots.push(v.clone());
interp.reserve_heap(ELEMENT_BYTES)?;
values.push(v);
}
None => {}
}
}
if matches!(compare, Value::Undefined) {
let mut keyed = Vec::with_capacity(values.len());
for v in values {
interp.use_fuel()?;
let s = interp.to_str(&v)?;
// Flattening a concatenation makes a new buffer, so it is charged. A key that is
// not an element is rooted so that collections count it.
let rope = s.buffer_addr().is_none();
let bytes = crate::heap::string_bytes(s.len());
if rope {
interp.reserve_heap(bytes)?;
}
let units = s.flat();
if rope {
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
}
if !matches!(v, Value::String(_)) {
interp.temp_roots.push(Value::String(s));
}
keyed.push((units, v));
}
merge_sort(interp, &mut keyed, |_, a, b| Ok(a.0 > b.0))?;
values = keyed.into_iter().map(|(_, v)| v).collect();
} else {
merge_sort(interp, &mut values, |interp, a, b| {
let r = interp.call(compare, &Value::Undefined, &[a.clone(), b.clone()])?;
Ok(interp.to_number(&r)? > 0.0)
})?;
}
let mut i = 0u64;
for v in values {
interp.use_fuel()?;
put_at(interp, o, i as f64, v)?;
i += 1;
}
for _ in 0..undefined {
interp.use_fuel()?;
put_at(interp, o, i as f64, Value::Undefined)?;
i += 1;
}
let mut it = Indices::down(interp, o, i, len)?;
while let Some(k) = it.next(interp)? {
delete_at(interp, o, k as f64)?;
}
Ok(())
}
// ---- searching ----
/// `Array.prototype.indexOf` (section 15.4.4.14).
fn index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = f64::from(length_of(interp, o)?);
if len == 0.0 {
return Ok(Value::Number(-1.0));
}
let n = if args.len() > 1 { to_integer(interp, &arg(args, 1))? } else { 0.0 };
if n >= len {
return Ok(Value::Number(-1.0));
}
let start = if n >= 0.0 { n } else { (len + n).max(0.0) };
let target = arg(args, 0);
let mut it = Indices::up(interp, o, start as u64, len as u64)?;
while let Some(k) = it.next(interp)? {
if let Some(v) = element(interp, o, k)?
&& strict_eq(&v, &target)
{
return Ok(number(k));
}
}
Ok(Value::Number(-1.0))
})
}
/// `Array.prototype.lastIndexOf` (section 15.4.4.15).
fn last_index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = f64::from(length_of(interp, o)?);
if len == 0.0 {
return Ok(Value::Number(-1.0));
}
let n = if args.len() > 1 { to_integer(interp, &arg(args, 1))? } else { len - 1.0 };
let from = if n >= 0.0 { n.min(len - 1.0) } else { len + n };
if from < 0.0 {
return Ok(Value::Number(-1.0));
}
let target = arg(args, 0);
let mut it = Indices::down(interp, o, 0, from as u64 + 1)?;
while let Some(k) = it.next(interp)? {
if let Some(v) = element(interp, o, k)?
&& strict_eq(&v, &target)
{
return Ok(number(k));
}
}
Ok(Value::Number(-1.0))
})
}
// ---- iteration ----
/// What an iteration method does with each callback result.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Each {
Every,
Some,
/// `forEach`.
Visit,
Map,
Filter,
}
/// `every`, `some`, `forEach`, `map` and `filter` (sections 15.4.4.16 to 15.4.4.20).
fn iterate(interp: &mut Interp, this: Value, args: &[Value], kind: Each) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = length_of(interp, o)?;
let callback = arg(args, 0);
require_callable(interp, &callback, "the callback")?;
let this_arg = arg(args, 1);
let a = match kind {
Each::Map => Some(new_array(interp, len)?),
Each::Filter => Some(new_array(interp, 0)?),
_ => None,
};
let roots: Vec<Value> = a.map(Value::Object).into_iter().collect();
interp.rooted(&roots, |interp| {
let mut to = 0u64;
let mut it = Indices::up(interp, o, 0, u64::from(len))?;
while let Some(k) = it.next(interp)? {
let Some(v) = element(interp, o, k)? else { continue };
let r = interp.call(&callback, &this_arg, &[v.clone(), number(k), Value::Object(o)])?;
match (kind, a) {
(Each::Every, _) if !r.truthy() => return Ok(Value::Bool(false)),
(Each::Some, _) if r.truthy() => return Ok(Value::Bool(true)),
(Each::Map, Some(a)) => create_at(interp, a, k as f64, r)?,
(Each::Filter, Some(a)) if r.truthy() => {
create_at(interp, a, to as f64, v)?;
to += 1;
}
_ => {}
}
}
Ok(match (kind, a) {
(Each::Every, _) => Value::Bool(true),
(Each::Some, _) => Value::Bool(false),
(_, Some(a)) => Value::Object(a),
_ => Value::Undefined,
})
})
})
}
fn every(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
iterate(interp, this, args, Each::Every)
}
fn some(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
iterate(interp, this, args, Each::Some)
}
fn for_each(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
iterate(interp, this, args, Each::Visit)
}
fn map(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
iterate(interp, this, args, Each::Map)
}
fn filter(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
iterate(interp, this, args, Each::Filter)
}
/// `reduce` and `reduceRight` (sections 15.4.4.21 and 15.4.4.22).
fn fold(interp: &mut Interp, this: Value, args: &[Value], right: bool) -> JsResult<Value> {
with_this(interp, &this, |interp, o| {
let len = u64::from(length_of(interp, o)?);
let callback = arg(args, 0);
require_callable(interp, &callback, "the callback")?;
let mut it = Indices::new(interp, o, 0, len, !right, vec![Map::Shift(0)])?;
let slot = interp.temp_roots.len();
interp.temp_roots.push(Value::Undefined);
// The accumulator is kept in a root slot while the elements are read.
let keep = |interp: &mut Interp, acc: &Value| {
if let Some(s) = interp.temp_roots.get_mut(slot) {
*s = acc.clone();
}
};
let r = (|| {
let mut acc = match args.get(1) {
Some(v) => v.clone(),
None => loop {
let Some(k) = it.next(interp)? else { return Err(interp.type_error("reduce of an empty array with no initial value")) };
if let Some(v) = element(interp, o, k)? {
break v;
}
},
};
keep(interp, &acc);
while let Some(k) = it.next(interp)? {
let Some(v) = element(interp, o, k)? else { continue };
acc = interp.call(&callback, &Value::Undefined, &[acc, v, number(k), Value::Object(o)])?;
keep(interp, &acc);
}
Ok(acc)
})();
interp.temp_roots.truncate(slot);
r
})
}
fn reduce(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
fold(interp, this, args, false)
}
fn reduce_right(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
fold(interp, this, args, true)
}