Skip to main content

submilli_engine/runtime/prelude/array/
mod.rs

1//! The submilli array's operations — the Rust port of the prelude's `Array`
2//! and `ArrayConstructor` methods.
3//!
4//! Unlike the string port, these work in wasmtime's value space: an array holds
5//! boxed object references, higher-order methods call back into a *guest*
6//! closure once per element, and the searching/`join`/default-`sort` paths
7//! re-enter each element's vtable (`toString`/`equals`). The `$Array`
8//! marshalling and host-fn registration live in [`install`]; the shared
9//! [`Closure`](crate::runtime::prelude::closure::Closure) handles callbacks.
10//! This file is the iteration and the element math.
11//!
12//! Mutators retain the receiver's identity and spare backing capacity. Methods
13//! that call guest code snapshot and root their inputs before callbacks run.
14
15mod install;
16pub(super) mod sort;
17
18pub(crate) use install::{array_like_type, declare_types};
19pub use install::{declare, install};
20pub(crate) use sort::merge_sort;
21
22use wasmtime::{Caller, StructRef, StructRefPre, Val};
23
24use crate::runtime::StoreData;
25use crate::runtime::array_storage::ArrayStorage;
26use crate::runtime::host::{host_boxed_number_vtable, write_submilli_array_struct};
27use crate::runtime::intrinsic_types::intrinsic_types;
28use crate::runtime::prelude::closure::Closure;
29use crate::runtime::prelude::collection::both_nan;
30use crate::runtime::prelude::iterator::{IterKind, IteratorSource, make_index_iterator};
31use crate::runtime::prelude::keep::{KeptValue, KeptValues, keep_all};
32use crate::runtime::prelude::vtable::{dispatch_vtable_slot, read_string_units};
33
34// ---------------------------------------------------------------------------
35// Marshalling helpers
36// ---------------------------------------------------------------------------
37
38/// Read an `$Array` `Val` into its boxed elements, snapshotting up front so a
39/// callback may mutate the source array during iteration.
40pub(super) fn read_array(
41    caller: &mut Caller<'_, StoreData>,
42    val: &Val,
43    _name: &str,
44) -> wasmtime::Result<Vec<Val>> {
45    crate::runtime::array_storage::ArrayStorage::read(caller, val)?.snapshot(caller)
46}
47
48/// [`read_array`] for a method that runs the program's code while it holds
49/// the elements: a callback, or an element's own `toString`. That code can
50/// drop them from the source array, leaving the snapshot the only thing that
51/// refers to them, so it is kept reachable. Element `equals` and `hash` are
52/// never the program's, so the searching methods read without keeping.
53pub(super) fn read_kept_array(
54    caller: &mut Caller<'_, StoreData>,
55    val: &Val,
56    name: &str,
57) -> wasmtime::Result<Vec<Val>> {
58    let elements = read_array(caller, val, name)?;
59    keep_all(caller, &elements)?;
60    Ok(elements)
61}
62
63/// Build a fresh `$Array` `Val` from already-boxed elements.
64fn build_array(caller: &mut Caller<'_, StoreData>, elements: &[Val]) -> wasmtime::Result<Val> {
65    let st = write_submilli_array_struct(caller, elements)?;
66    Ok(Val::AnyRef(Some(st.to_anyref())))
67}
68
69/// Replace the live elements, retaining capacity and clearing removed slots.
70fn replace_elements(
71    caller: &mut Caller<'_, StoreData>,
72    receiver: &Val,
73    elements: &[Val],
74) -> wasmtime::Result<()> {
75    crate::runtime::array_storage::ArrayStorage::read(caller, receiver)?.replace(caller, elements)
76}
77
78/// Box an `f64` into a `$boxed_number` object (for iterator indices).
79fn box_number(caller: &mut Caller<'_, StoreData>, n: f64) -> wasmtime::Result<Val> {
80    let boxed = intrinsic_types(&mut *caller)?.boxed_number.clone();
81    let vtable = host_boxed_number_vtable(caller)?;
82    let pre = StructRefPre::new(&mut *caller, boxed);
83    let st = StructRef::new(&mut *caller, &pre, &[vtable, Val::F64(n.to_bits())])?;
84    Ok(Val::AnyRef(Some(st.to_anyref())))
85}
86
87/// Whether `val` is a (non-null) `$Array` — backs `flat`/`flatMap` flattening.
88pub(super) fn is_array(caller: &mut Caller<'_, StoreData>, val: &Val) -> wasmtime::Result<bool> {
89    let Val::AnyRef(Some(any)) = val else {
90        return Ok(false);
91    };
92    let Some(st) = any.as_struct(&mut *caller)? else {
93        return Ok(false);
94    };
95    let array_ty = intrinsic_types(&mut *caller)?.array.clone();
96    st.matches_ty(&*caller, &array_ty)
97}
98
99fn is_null(v: &Val) -> bool {
100    matches!(v, Val::AnyRef(None))
101}
102
103/// The element's `toString()` (vtable slot 0) as code units — `join` and the
104/// default `sort` order. `null` has no vtable, so each caller decides its text:
105/// JavaScript joins it as `""` but sorts it as `"null"`.
106async fn element_to_string(
107    caller: &mut Caller<'_, StoreData>,
108    elem: Val,
109) -> wasmtime::Result<Vec<u16>> {
110    if is_null(&elem) {
111        return Err(wasmtime::Error::msg("array element toString: null element"));
112    }
113    let s = dispatch_vtable_slot(caller, &elem, 0, &[]).await?;
114    read_string_units(caller, &s, "array element toString result")
115}
116
117/// An element's text in `join`: `null` joins as the empty string, as in JavaScript
118/// (`[1, null].join()` is `"1,"`).
119async fn join_text(caller: &mut Caller<'_, StoreData>, elem: Val) -> wasmtime::Result<Vec<u16>> {
120    if super::undefined::is_nullish(caller, &elem)? {
121        return Ok(Vec::new());
122    }
123    element_to_string(caller, elem).await
124}
125
126/// Whether `slot` holds `target` — `indexOf`/`lastIndexOf`/`includes`. `null`
127/// equals only `null` (`[1, null].indexOf(null)` is `1`) and is decided here: a null
128/// slot has no vtable, and a class's `equals` cannot take a null argument.
129async fn element_matches(
130    caller: &mut Caller<'_, StoreData>,
131    slot: Val,
132    target: Val,
133) -> wasmtime::Result<bool> {
134    if is_null(&slot) || is_null(&target) {
135        return Ok(is_null(&slot) && is_null(&target));
136    }
137    let left_undefined = super::undefined::is_undefined(caller, &slot)?;
138    let right_undefined = super::undefined::is_undefined(caller, &target)?;
139    if left_undefined || right_undefined {
140        return Ok(left_undefined && right_undefined);
141    }
142    element_equals(caller, slot, target).await
143}
144
145/// Whether `slot` equals `target` via `slot`'s `equals` (vtable slot 2).
146async fn element_equals(
147    caller: &mut Caller<'_, StoreData>,
148    slot: Val,
149    target: Val,
150) -> wasmtime::Result<bool> {
151    match dispatch_vtable_slot(caller, &slot, 2, &[target]).await? {
152        Val::I32(b) => Ok(b != 0),
153        other => Err(wasmtime::Error::msg(format!(
154            "array element equals returned {other:?}, expected i32"
155        ))),
156    }
157}
158
159// ---------------------------------------------------------------------------
160// Index math (ported 1:1 from the Wasm bodies)
161// ---------------------------------------------------------------------------
162
163/// `i32.trunc_sat_f64_s` — Rust's `as i32` saturates (NaN → 0) identically.
164fn trunc_sat(x: f64) -> i32 {
165    x as i32
166}
167
168/// JS slice/fill/copyWithin index: `if x<0 {x+=len}; clamp to [0, len]`.
169fn norm_clamp(x: f64, len: i32) -> i32 {
170    let mut i = trunc_sat(x);
171    if i < 0 {
172        i += len;
173    }
174    i.clamp(0, len)
175}
176
177/// `at` index: from-end on negatives, `None` when out of `[0, len)`.
178fn at_index(x: f64, len: i32) -> Option<usize> {
179    let mut i = trunc_sat(x);
180    if i < 0 {
181        i += len;
182    }
183    if i < 0 || i >= len {
184        None
185    } else {
186        Some(i as usize)
187    }
188}
189
190/// Forward search start (`indexOf`/`includes`): from-end on negatives, floored at 0.
191fn fwd_from(x: f64, len: i32) -> i32 {
192    let mut fi = trunc_sat(x);
193    if fi < 0 {
194        fi += len;
195    }
196    fi.max(0)
197}
198
199/// Backward search start (`lastIndexOf`): from-end on negatives, `None` below 0,
200/// capped at `len-1`.
201fn last_from(x: f64, len: i32) -> Option<i32> {
202    let mut fi = trunc_sat(x);
203    if fi < 0 {
204        fi += len;
205    }
206    if fi < 0 {
207        return None;
208    }
209    Some(fi.min(len - 1))
210}
211
212// ---------------------------------------------------------------------------
213// Accessors
214// ---------------------------------------------------------------------------
215
216/// `at(index)`: one element read in place, `undefined` out of range.
217fn at(caller: &mut Caller<'_, StoreData>, receiver: &Val, index: f64) -> wasmtime::Result<Val> {
218    let storage = ArrayStorage::read(caller, receiver)?;
219    match at_index(index, storage.len as i32) {
220        Some(i) => storage.get(caller, i as u32),
221        None => super::undefined::value(caller),
222    }
223}
224
225/// `slice(start, end)`: only the kept range is read.
226fn slice(
227    caller: &mut Caller<'_, StoreData>,
228    receiver: &Val,
229    start: f64,
230    end: f64,
231) -> wasmtime::Result<Vec<Val>> {
232    let storage = ArrayStorage::read(caller, receiver)?;
233    let len = storage.len as i32;
234    let si = norm_clamp(start, len);
235    let count = (norm_clamp(end, len) - si).max(0);
236    storage.range(caller, si as u32, count as u32)
237}
238
239/// `concat(...others: T[][])`: this array's elements followed by every element
240/// of each `others` array.
241fn concat(
242    caller: &mut Caller<'_, StoreData>,
243    mut acc: Vec<Val>,
244    others: &Val,
245) -> wasmtime::Result<Vec<Val>> {
246    // As in JS, an array argument contributes its elements and anything else
247    // is appended as one element.
248    for item in read_array(caller, others, "Array#concat")? {
249        if is_array(caller, &item)? {
250            acc.extend(read_array(caller, &item, "Array#concat")?);
251        } else {
252            acc.push(item);
253        }
254    }
255    Ok(acc)
256}
257
258async fn index_of(
259    caller: &mut Caller<'_, StoreData>,
260    elements: Vec<Val>,
261    target: Val,
262    from: f64,
263) -> wasmtime::Result<f64> {
264    let len = elements.len() as i32;
265    let mut i = fwd_from(from, len);
266    while i < len {
267        let e = elements[i as usize];
268        if element_matches(caller, e, target).await? {
269            return Ok(i as f64);
270        }
271        i += 1;
272    }
273    Ok(-1.0)
274}
275
276async fn last_index_of(
277    caller: &mut Caller<'_, StoreData>,
278    elements: Vec<Val>,
279    target: Val,
280    from: f64,
281) -> wasmtime::Result<f64> {
282    let len = elements.len() as i32;
283    let Some(mut i) = last_from(from, len) else {
284        return Ok(-1.0);
285    };
286    while i >= 0 {
287        let e = elements[i as usize];
288        if element_matches(caller, e, target).await? {
289            return Ok(i as f64);
290        }
291        i -= 1;
292    }
293    Ok(-1.0)
294}
295
296async fn includes(
297    caller: &mut Caller<'_, StoreData>,
298    elements: Vec<Val>,
299    target: Val,
300    from: f64,
301) -> wasmtime::Result<bool> {
302    let len = elements.len() as i32;
303    let mut i = fwd_from(from, len);
304    while i < len {
305        let e = elements[i as usize];
306        // SameValueZero, unlike `indexOf`'s `===`: `NaN` finds `NaN`.
307        if element_matches(caller, e, target).await? || both_nan(caller, &e, &target)? {
308            return Ok(true);
309        }
310        i += 1;
311    }
312    Ok(false)
313}
314
315async fn join(
316    caller: &mut Caller<'_, StoreData>,
317    elements: Vec<Val>,
318    sep: Vec<u16>,
319) -> wasmtime::Result<Vec<u16>> {
320    if elements.is_empty() {
321        return Ok(Vec::new());
322    }
323    let mut acc = join_text(caller, elements[0]).await?;
324    for &e in &elements[1..] {
325        acc.extend_from_slice(&sep);
326        let s = join_text(caller, e).await?;
327        acc.extend_from_slice(&s);
328    }
329    Ok(acc)
330}
331
332// ---------------------------------------------------------------------------
333// Mutators (snapshot transforms preserve receiver identity)
334// ---------------------------------------------------------------------------
335
336fn push(
337    caller: &mut Caller<'_, StoreData>,
338    receiver: &Val,
339    items: Vec<Val>,
340) -> wasmtime::Result<f64> {
341    let mut storage = ArrayStorage::read(caller, receiver)?;
342    let mut length = f64::from(storage.len);
343    for item in items {
344        length = storage.push(caller, item)?;
345    }
346    Ok(length)
347}
348
349/// `xs.length = value`: truncates in place. JavaScript would also grow the
350/// array with holes, which Submilli arrays cannot hold, so growth throws.
351pub(crate) fn set_length(
352    caller: &mut Caller<'_, StoreData>,
353    receiver: &Val,
354    value: f64,
355) -> wasmtime::Result<()> {
356    if !(value >= 0.0 && value.fract() == 0.0 && value <= f64::from(u32::MAX)) {
357        return Err(crate::runtime::host::range_error("Invalid array length"));
358    }
359    let mut storage = ArrayStorage::read(caller, receiver)?;
360    if value > f64::from(storage.len) {
361        return Err(crate::runtime::host::range_error(format!(
362            "Cannot grow an array by writing length {value}; its length is {}",
363            storage.len
364        )));
365    }
366    storage.resize(caller, value as usize, Val::null_any_ref())
367}
368
369/// `pop()`: the last element, removed in place; `undefined` when empty.
370fn pop(caller: &mut Caller<'_, StoreData>, receiver: &Val) -> wasmtime::Result<Val> {
371    let popped = ArrayStorage::read(caller, receiver)?.pop(caller)?;
372    match popped {
373        Some(value) => Ok(value),
374        None => super::undefined::value(caller),
375    }
376}
377
378fn shift(
379    caller: &mut Caller<'_, StoreData>,
380    receiver: &Val,
381    mut elements: Vec<Val>,
382) -> wasmtime::Result<Val> {
383    if elements.is_empty() {
384        return super::undefined::value(caller);
385    }
386    let first = elements.remove(0);
387    replace_elements(caller, receiver, &elements)?;
388    Ok(first)
389}
390
391fn unshift(
392    caller: &mut Caller<'_, StoreData>,
393    receiver: &Val,
394    elements: Vec<Val>,
395    mut items: Vec<Val>,
396) -> wasmtime::Result<f64> {
397    items.extend(elements);
398    let n = items.len() as f64;
399    replace_elements(caller, receiver, &items)?;
400    Ok(n)
401}
402
403fn reverse(
404    caller: &mut Caller<'_, StoreData>,
405    receiver: &Val,
406    mut elements: Vec<Val>,
407) -> wasmtime::Result<Val> {
408    elements.reverse();
409    replace_elements(caller, receiver, &elements)?;
410    Ok(*receiver)
411}
412
413fn fill(
414    caller: &mut Caller<'_, StoreData>,
415    receiver: &Val,
416    mut elements: Vec<Val>,
417    value: Val,
418    start: f64,
419    end: f64,
420) -> wasmtime::Result<Val> {
421    let len = elements.len() as i32;
422    let si = norm_clamp(start, len);
423    let ei = norm_clamp(end, len);
424    for slot in elements.iter_mut().take(ei as usize).skip(si as usize) {
425        *slot = value;
426    }
427    replace_elements(caller, receiver, &elements)?;
428    Ok(*receiver)
429}
430
431fn copy_within(
432    caller: &mut Caller<'_, StoreData>,
433    receiver: &Val,
434    mut elements: Vec<Val>,
435    target: f64,
436    start: f64,
437    end: f64,
438) -> wasmtime::Result<Val> {
439    let len = elements.len() as i32;
440    let ti = norm_clamp(target, len);
441    let si = norm_clamp(start, len);
442    let ei = norm_clamp(end, len);
443    let mut count = (ei - si).max(0);
444    let rem = len - ti;
445    if count > rem {
446        count = rem;
447    }
448    // Copy the source span first so overlapping ranges stay memmove-correct.
449    let src: Vec<Val> = elements[si as usize..(si + count) as usize].to_vec();
450    for (k, v) in src.into_iter().enumerate() {
451        elements[ti as usize + k] = v;
452    }
453    replace_elements(caller, receiver, &elements)?;
454    Ok(*receiver)
455}
456
457/// The removed span and the post-splice element list — shared by `splice` and
458/// `toSpliced`.
459fn splice_parts(
460    elements: &[Val],
461    start: f64,
462    delete_count: f64,
463    items: Vec<Val>,
464) -> (Vec<Val>, Vec<Val>) {
465    let len = elements.len() as i32;
466    let si = norm_clamp(start, len);
467    let mut dc = trunc_sat(delete_count).max(0);
468    let rem = len - si;
469    if dc > rem {
470        dc = rem;
471    }
472    let (si, dc) = (si as usize, dc as usize);
473    let removed = elements[si..si + dc].to_vec();
474    let mut result = Vec::with_capacity(elements.len() - dc + items.len());
475    result.extend_from_slice(&elements[..si]);
476    result.extend(items);
477    result.extend_from_slice(&elements[si + dc..]);
478    (removed, result)
479}
480
481fn splice(
482    caller: &mut Caller<'_, StoreData>,
483    receiver: &Val,
484    elements: Vec<Val>,
485    start: f64,
486    delete_count: f64,
487    items: Vec<Val>,
488) -> wasmtime::Result<Val> {
489    let (removed, result) = splice_parts(&elements, start, delete_count, items);
490    // Built while the receiver still holds the removed elements: once they are
491    // swapped out nothing else does, and an allocation may collect.
492    let removed = build_array(caller, &removed)?;
493    replace_elements(caller, receiver, &result)?;
494    Ok(removed)
495}
496
497async fn sort(
498    caller: &mut Caller<'_, StoreData>,
499    receiver: &Val,
500    mut elements: Vec<Val>,
501    cmp: Option<Closure>,
502) -> wasmtime::Result<Val> {
503    sort_elems(caller, &mut elements, cmp.as_ref()).await?;
504    // Re-read the storage: the comparator may have pushed or popped.
505    ArrayStorage::read(caller, receiver)?.overwrite_prefix(caller, &elements)?;
506    Ok(*receiver)
507}
508
509// ---------------------------------------------------------------------------
510// Immutable variants (build a fresh result; receiver untouched)
511// ---------------------------------------------------------------------------
512
513fn to_reversed(mut elements: Vec<Val>) -> Vec<Val> {
514    elements.reverse();
515    elements
516}
517
518async fn to_sorted(
519    caller: &mut Caller<'_, StoreData>,
520    mut elements: Vec<Val>,
521    cmp: Option<Closure>,
522) -> wasmtime::Result<Vec<Val>> {
523    sort_elems(caller, &mut elements, cmp.as_ref()).await?;
524    Ok(elements)
525}
526
527/// Stable sort by the program's comparator, or by element `toString()` for the
528/// default order.
529async fn sort_elems(
530    caller: &mut Caller<'_, StoreData>,
531    elements: &mut Vec<Val>,
532    cmp: Option<&Closure>,
533) -> wasmtime::Result<()> {
534    let original_len = elements.len();
535    let mut defined_len = 0usize;
536    for index in 0..original_len {
537        let element = *elements
538            .get(index)
539            .ok_or_else(|| crate::runtime::host::fatal_host_error("sort: invalid element index"))?;
540        if !super::undefined::is_undefined(caller, &element)? {
541            let slot = elements.get_mut(defined_len).ok_or_else(|| {
542                crate::runtime::host::fatal_host_error("sort: invalid output index")
543            })?;
544            *slot = element;
545            defined_len += 1;
546        }
547    }
548    elements.truncate(defined_len);
549    match cmp {
550        Some(cmp) => merge_sort(caller, elements, cmp, |_, element| Ok(element)).await?,
551        None => sort::sort_by_string(caller, elements).await?,
552    }
553    if defined_len != original_len {
554        let undefined = super::undefined::value(caller)?;
555        elements
556            .try_reserve_exact(original_len - defined_len)
557            .map_err(crate::runtime::host::fatal_host_error)?;
558        elements.resize(original_len, undefined);
559    }
560    Ok(())
561}
562
563/// `None` when `index` is out of range — [`install`] raises the catchable
564/// `Error("index out of range")`.
565fn with(elements: &[Val], index: f64, value: Val) -> Option<Vec<Val>> {
566    let i = at_index(index, elements.len() as i32)?;
567    let mut new = elements.to_vec();
568    new[i] = value;
569    Some(new)
570}
571
572// ---------------------------------------------------------------------------
573// Higher-order methods (re-enter a guest closure per element)
574// ---------------------------------------------------------------------------
575
576/// An element callback, called with `(value, index, array)` as TypeScript
577/// types it — or `(value, index)` without an array, as `Array.from`'s `mapFn`.
578/// Its slot is erased, so it may be any function: it is passed only the
579/// arguments it reads, and the index is boxed only when it reads that.
580pub(in crate::runtime::prelude) struct ElementCallback<'a> {
581    f: &'a Closure,
582    reads: usize,
583    array: Option<Val>,
584}
585
586impl<'a> ElementCallback<'a> {
587    pub(in crate::runtime::prelude) fn new(
588        caller: &mut Caller<'_, StoreData>,
589        f: &'a Closure,
590        array: Option<Val>,
591    ) -> wasmtime::Result<Self> {
592        let reads = f.arguments_read(caller)?;
593        Ok(Self { f, reads, array })
594    }
595
596    /// Call on the element at `index`, after `leading` (`reduce`'s accumulator).
597    pub(in crate::runtime::prelude) async fn call(
598        &self,
599        caller: &mut Caller<'_, StoreData>,
600        leading: Option<Val>,
601        value: Val,
602        index: usize,
603    ) -> wasmtime::Result<Val> {
604        let mut args: Vec<Val> = leading.into_iter().chain([value]).collect();
605        if args.len() < self.reads {
606            args.push(box_number(caller, index as f64)?);
607        }
608        if let Some(array) = self.array
609            && args.len() < self.reads
610        {
611            args.push(array);
612        }
613        self.f.call_dynamic(caller, &args).await
614    }
615
616    pub(in crate::runtime::prelude) async fn test(
617        &self,
618        caller: &mut Caller<'_, StoreData>,
619        value: Val,
620        index: usize,
621    ) -> wasmtime::Result<bool> {
622        let result = self.call(caller, None, value, index).await?;
623        super::value::truthy(caller, &result)
624    }
625}
626
627/// `Array.prototype.forEach`: invoke `f` once per element, left to right.
628pub async fn for_each(
629    caller: &mut Caller<'_, StoreData>,
630    array: Val,
631    elements: Vec<Val>,
632    f: &Closure,
633) -> wasmtime::Result<()> {
634    let f = ElementCallback::new(caller, f, Some(array))?;
635    for (index, elem) in elements.into_iter().enumerate() {
636        f.call(caller, None, elem, index).await?;
637    }
638    Ok(())
639}
640
641async fn map(
642    caller: &mut Caller<'_, StoreData>,
643    array: Val,
644    elements: Vec<Val>,
645    f: &Closure,
646) -> wasmtime::Result<Vec<Val>> {
647    let f = ElementCallback::new(caller, f, Some(array))?;
648    let mut out = KeptValues::with_capacity(caller, elements.len())?;
649    for (index, elem) in elements.into_iter().enumerate() {
650        let mapped = f.call(caller, None, elem, index).await?;
651        out.push(caller, mapped)?;
652    }
653    Ok(out.values().to_vec())
654}
655
656async fn filter(
657    caller: &mut Caller<'_, StoreData>,
658    array: Val,
659    elements: Vec<Val>,
660    pred: &Closure,
661) -> wasmtime::Result<Vec<Val>> {
662    let pred = ElementCallback::new(caller, pred, Some(array))?;
663    let mut out = Vec::new();
664    for (index, elem) in elements.into_iter().enumerate() {
665        if pred.test(caller, elem, index).await? {
666            out.push(elem);
667        }
668    }
669    Ok(out)
670}
671
672/// `reduce`, or with `reverse` `reduceRight`: fold `(acc, value, index, array)`
673/// over the elements.
674async fn reduce(
675    caller: &mut Caller<'_, StoreData>,
676    array: Val,
677    elements: Vec<Val>,
678    f: &Closure,
679    mut acc: Val,
680    reverse: bool,
681) -> wasmtime::Result<Val> {
682    let f = ElementCallback::new(caller, f, Some(array))?;
683    let mut indexed: Vec<(usize, Val)> = elements.into_iter().enumerate().collect();
684    if reverse {
685        indexed.reverse();
686    }
687    // The next call boxes its index before it passes the accumulator on.
688    let kept_acc = KeptValue::new(caller)?;
689    for (index, elem) in indexed {
690        acc = f.call(caller, Some(acc), elem, index).await?;
691        kept_acc.set(caller, acc)?;
692    }
693    Ok(acc)
694}
695
696/// Index of the first (or, with `reverse`, last) element matching `pred`.
697async fn find_match(
698    caller: &mut Caller<'_, StoreData>,
699    array: Val,
700    elements: &[Val],
701    pred: &Closure,
702    reverse: bool,
703) -> wasmtime::Result<Option<usize>> {
704    let pred = ElementCallback::new(caller, pred, Some(array))?;
705    let order: Vec<usize> = if reverse {
706        (0..elements.len()).rev().collect()
707    } else {
708        (0..elements.len()).collect()
709    };
710    for i in order {
711        if pred.test(caller, elements[i], i).await? {
712            return Ok(Some(i));
713        }
714    }
715    Ok(None)
716}
717
718async fn some(
719    caller: &mut Caller<'_, StoreData>,
720    array: Val,
721    elements: Vec<Val>,
722    pred: &Closure,
723) -> wasmtime::Result<bool> {
724    let pred = ElementCallback::new(caller, pred, Some(array))?;
725    for (index, elem) in elements.into_iter().enumerate() {
726        if pred.test(caller, elem, index).await? {
727            return Ok(true);
728        }
729    }
730    Ok(false)
731}
732
733async fn every(
734    caller: &mut Caller<'_, StoreData>,
735    array: Val,
736    elements: Vec<Val>,
737    pred: &Closure,
738) -> wasmtime::Result<bool> {
739    let pred = ElementCallback::new(caller, pred, Some(array))?;
740    for (index, elem) in elements.into_iter().enumerate() {
741        if !pred.test(caller, elem, index).await? {
742            return Ok(false);
743        }
744    }
745    Ok(true)
746}
747
748/// Flatten without native recursion; each frame retains its unvisited siblings.
749/// Bound nesting independently of the requested flattening depth.
750fn flat_into(
751    caller: &mut Caller<'_, StoreData>,
752    elements: Vec<Val>,
753    depth: i32,
754    out: &mut Vec<Val>,
755) -> wasmtime::Result<()> {
756    let mut frames = vec![(elements.into_iter(), depth)];
757    while let Some((elements, depth)) = frames.last_mut() {
758        let Some(elem) = elements.next() else {
759            frames.pop();
760            continue;
761        };
762        let depth = *depth;
763        if depth > 0 && is_array(caller, &elem)? {
764            if frames.len() >= crate::runtime::MAX_VTABLE_WALK_DEPTH as usize {
765                return Err(crate::runtime::host::range_error(format!(
766                    "Array#flat exceeds {} levels of nesting; flatten fewer levels",
767                    crate::runtime::MAX_VTABLE_WALK_DEPTH,
768                )));
769            }
770            let sub = read_array(caller, &elem, "Array#flat")?;
771            frames
772                .try_reserve(1)
773                .map_err(crate::runtime::host::fatal_host_error)?;
774            frames.push((sub.into_iter(), depth - 1));
775        } else {
776            out.try_reserve(1)
777                .map_err(crate::runtime::host::fatal_host_error)?;
778            out.push(elem);
779        }
780    }
781    Ok(())
782}
783
784async fn flat_map(
785    caller: &mut Caller<'_, StoreData>,
786    array: Val,
787    elements: Vec<Val>,
788    f: &Closure,
789) -> wasmtime::Result<Vec<Val>> {
790    let f = ElementCallback::new(caller, f, Some(array))?;
791    let kept_mapped = KeptValue::new(caller)?;
792    let mut out = KeptValues::with_capacity(caller, elements.len())?;
793    for (index, elem) in elements.into_iter().enumerate() {
794        let mapped = f.call(caller, None, elem, index).await?;
795        // The callback may reuse the array it returns, so its elements are
796        // kept themselves; the array holds them while room is made.
797        kept_mapped.set(caller, mapped)?;
798        let flattened = read_array(caller, &mapped, "Array#flatMap callback result")?;
799        out.extend(caller, &flattened)?;
800    }
801    Ok(out.values().to_vec())
802}
803
804// ---------------------------------------------------------------------------
805// Iterators (keys / values / entries)
806// ---------------------------------------------------------------------------
807
808/// One step of an Array iterator: the boxed index + element at `pos`. Re-reads
809/// the live backing each call, so a `push` mid-iteration is observed. `payload`
810/// is the `$Array` wrapper itself (not its raw backing), passed through the
811/// shared cursor by [`make_index_iterator`].
812fn array_step(
813    caller: &mut Caller<'_, StoreData>,
814    payload: &Val,
815    pos: i32,
816) -> wasmtime::Result<Option<(Val, Val)>> {
817    let storage = crate::runtime::array_storage::ArrayStorage::read(caller, payload)?;
818    let backing = storage.backing;
819    if pos < 0 || pos as u32 >= storage.len {
820        return Ok(None);
821    }
822    let elem = backing.get(&mut *caller, pos as u32)?;
823    let idx = box_number(caller, pos as f64)?;
824    Ok(Some((idx, elem)))
825}
826
827pub fn values(caller: &mut Caller<'_, StoreData>, array: &Val) -> wasmtime::Result<Val> {
828    make_index_iterator(
829        caller,
830        IteratorSource::Array,
831        *array,
832        IterKind::Values,
833        array_step,
834    )
835}
836
837fn keys(caller: &mut Caller<'_, StoreData>, array: &Val) -> wasmtime::Result<Val> {
838    make_index_iterator(
839        caller,
840        IteratorSource::Array,
841        *array,
842        IterKind::Keys,
843        array_step,
844    )
845}
846
847fn entries(caller: &mut Caller<'_, StoreData>, array: &Val) -> wasmtime::Result<Val> {
848    make_index_iterator(
849        caller,
850        IteratorSource::Array,
851        *array,
852        IterKind::Entries,
853        array_step,
854    )
855}
856
857// ---------------------------------------------------------------------------
858// `ArrayConstructor#from`
859// ---------------------------------------------------------------------------
860
861/// `ArrayConstructor#from(src, mapFn?) -> T[]`. Materializes any iterable
862/// (mirroring the Wasm `collect_iterable`): an `$Array` → its elements; a
863/// `$string` → code points; a `$Map` → `[key, value]` pairs via its `entries()`
864/// cursor; a `$Set` → its `values()` cursor; any other value → drive the
865/// iterator protocol (an `iterator()` method if present, else the value itself).
866/// `mapFn` (arity-1) applies per element on every path, interleaved with
867/// iteration like the Wasm body.
868pub(super) async fn from(
869    caller: &mut Caller<'_, StoreData>,
870    src: &Val,
871    map_fn: &Val,
872) -> wasmtime::Result<Val> {
873    use crate::runtime::prelude::closure;
874    use crate::runtime::prelude::collection::{is_a, object_field, string_code_points, unbox_bool};
875
876    let map_closure = if super::undefined::is_undefined(caller, map_fn)? {
877        None
878    } else {
879        Some(closure::read_callback(caller, map_fn, "Array.from mapFn")?)
880    };
881    let map_fn = match &map_closure {
882        Some(c) => Some(ElementCallback::new(caller, c, None)?),
883        None => None,
884    };
885    let intr = intrinsic_types(&mut *caller)?;
886    let mut out = KeptValues::with_capacity(caller, 0)?;
887
888    if is_a(caller, src, &intr.array)? {
889        // Drive the live backing like the `values` cursor (`array_step`): re-read
890        // length and element each step, so a `mapFn` that appends to the source
891        // mid-iteration is observed — snapshotting would drop those elements.
892        let mut pos: u32 = 0;
893        loop {
894            let storage = crate::runtime::array_storage::ArrayStorage::read(caller, src)?;
895            let backing = storage.backing;
896            if pos >= storage.len {
897                break;
898            }
899            out.reserve(caller, 1)?;
900            let elem = backing.get(&mut *caller, pos)?;
901            let mapped = apply_map(caller, &map_fn, elem, out.values().len()).await?;
902            out.push(caller, mapped)?;
903            pos += 1;
904        }
905        return build_array(caller, out.values());
906    }
907    if is_a(caller, src, &intr.string)? {
908        let cps = string_code_points(caller, src)?;
909        out.reserve(caller, cps.len())?;
910        for cp in cps {
911            let mapped = apply_map(caller, &map_fn, cp, out.values().len()).await?;
912            out.push(caller, mapped)?;
913        }
914        return build_array(caller, out.values());
915    }
916
917    // The iterator and each object its `next()` returns come from guest calls.
918    let kept_iterator = KeptValue::new(caller)?;
919    let kept_result = KeptValue::new(caller)?;
920    let it = if is_a(
921        caller,
922        src,
923        &super::map::map_backing_struct(caller.engine(), &intr)?,
924    )? {
925        super::map::entries(caller, src)?
926    } else if is_a(
927        caller,
928        src,
929        &super::set::set_backing_struct(caller.engine(), &intr)?,
930    )? {
931        super::set::values(caller, src)?
932    } else if is_a(caller, src, &intr.uint8_array)? {
933        super::uint8array::values(caller, src)?
934    } else if let Some(iter_method) = object_field(caller, src, "iterator")? {
935        let c = closure::read(caller, &iter_method, "Array.from iterable")?;
936        c.call_with_receiver(caller, *src, &[]).await?
937    } else if object_field(caller, src, "next")?.is_none()
938        && let Some(length) = object_field(caller, src, "length")?
939    {
940        let length = array_like_length(caller, &length)?;
941        for index in 0..length {
942            out.reserve(caller, 1)?;
943            let hole = super::undefined::value(caller)?;
944            let mapped = apply_map(caller, &map_fn, hole, index).await?;
945            out.push(caller, mapped)?;
946        }
947        return build_array(caller, out.values());
948    } else {
949        *src
950    };
951    kept_iterator.set(caller, it)?;
952
953    let next = object_field(caller, &it, "next")?
954        .ok_or_else(|| wasmtime::Error::msg("Array.from: source is not iterable"))?;
955    let next_closure = closure::read(caller, &next, "Array.from iterator")?;
956    loop {
957        out.reserve(caller, 1)?;
958        let result = next_closure.call_with_receiver(caller, it, &[]).await?;
959        kept_result.set(caller, result)?;
960        let done = object_field(caller, &result, "done")?
961            .ok_or_else(|| wasmtime::Error::msg("Array.from: iterator result missing `done`"))?;
962        if unbox_bool(caller, &done)? {
963            break;
964        }
965        let value = object_field(caller, &result, "value")?
966            .ok_or_else(|| wasmtime::Error::msg("Array.from: iterator result missing `value`"))?;
967        let mapped = apply_map(caller, &map_fn, value, out.values().len()).await?;
968        out.push(caller, mapped)?;
969    }
970    build_array(caller, out.values())
971}
972
973/// An array-like's `length` as JavaScript's `ToLength` reads it for
974/// `Array.from`: truncated toward zero, with `NaN` and negatives as `0`. A
975/// length past the largest array is a `RangeError`.
976fn array_like_length(caller: &mut Caller<'_, StoreData>, length: &Val) -> wasmtime::Result<usize> {
977    let not_a_number = || wasmtime::Error::msg("Array.from: `length` is not a number");
978    let Val::AnyRef(Some(any)) = length else {
979        return Err(not_a_number());
980    };
981    let st = any.as_struct(&mut *caller)?.ok_or_else(not_a_number)?;
982    let Val::F64(bits) = st.field(&mut *caller, 1)? else {
983        return Err(not_a_number());
984    };
985    let length = f64::from_bits(bits);
986    if length.is_nan() || length <= 0.0 {
987        return Ok(0);
988    }
989    let length = length.trunc();
990    let invalid = || crate::runtime::host::range_error("Invalid array length");
991    if length > f64::from(u32::MAX) {
992        return Err(invalid());
993    }
994    usize::try_from(length as u32).map_err(|_| invalid())
995}
996
997async fn apply_map(
998    caller: &mut Caller<'_, StoreData>,
999    map_fn: &Option<ElementCallback<'_>>,
1000    value: Val,
1001    index: usize,
1002) -> wasmtime::Result<Val> {
1003    match map_fn {
1004        Some(f) => f.call(caller, None, value, index).await,
1005        None => Ok(value),
1006    }
1007}