Skip to main content

nodejs/stdlib/
typedarray.rs

1//! JavaScript typed arrays (`Uint8Array`/`Int8Array`/…/`Float64Array`),
2//! `ArrayBuffer`, `WeakRef`, and `TextEncoder`/`TextDecoder`.
3//!
4//! A typed array is a plain object tagged `@@native = "TypedArray"` carrying its
5//! kind (`@@kind`), its elements as a hidden `@@elems` array of numbers, and the
6//! enumerable `length`/`byteLength`/`BYTES_PER_ELEMENT` data properties JS code
7//! reads directly. Element indexing (`ta[i]` get/set) is special-cased in
8//! `builtins::get_property`/`set_property` via `elem_get`/`elem_set` here, which
9//! also apply each kind's coercion (integer wrap / clamp / float).
10//!
11//! `WeakRef` holds a *strong* reference (`deref()` always returns the target) —
12//! node-js has no GC of JS objects, so this is observably correct for the
13//! express dependency tree (object-inspect/qs/side-channel only ever `deref()`).
14
15use crate::host::{with_host, JsObj};
16use fusevm::Value;
17use indexmap::IndexMap;
18
19pub const STATIC_METHODS: &[&str] = &["from", "of", "isView"];
20
21/// The methods installed on the real `Uint8Array.prototype` object (as
22/// `@proto:Uint8Array:<m>` thunks), so `Uint8Array.prototype.slice.call(x)`
23/// keeps working now that the prototype is an object rather than a `Builtin`
24/// namespace whose every property read synthesized a thunk.
25pub const PROTOTYPE_METHODS: &[&str] = &[
26    "at",
27    "copyWithin",
28    "entries",
29    "every",
30    "fill",
31    "filter",
32    "find",
33    "findIndex",
34    "findLast",
35    "findLastIndex",
36    "forEach",
37    "includes",
38    "indexOf",
39    "join",
40    "keys",
41    "lastIndexOf",
42    "map",
43    "reduce",
44    "reduceRight",
45    "reverse",
46    "set",
47    "slice",
48    "some",
49    "sort",
50    "subarray",
51    "toString",
52    "values",
53];
54
55/// The eleven element kinds plus `ArrayBuffer` (which carries only a byte
56/// length).
57pub fn is_ctor(name: &str) -> bool {
58    ELEMENT_KINDS.contains(&name) || name == "ArrayBuffer"
59}
60
61/// The element kinds, each of which gets its own real prototype object whose
62/// parent is the shared `%TypedArray%.prototype`. `Uint8Array` leads because
63/// `Buffer.prototype` chains onto it.
64///
65/// `BigInt64Array`/`BigUint64Array` are here too, and they are not
66/// interchangeable with the rest: their elements are BigInts, so a Number
67/// written into one is a `TypeError` and a `Number`-kind view will not accept
68/// one either (`coerce_val`).
69pub const ELEMENT_KINDS: &[&str] = &[
70    "Uint8Array",
71    "Int8Array",
72    "Uint8ClampedArray",
73    "Int16Array",
74    "Uint16Array",
75    "Int32Array",
76    "Uint32Array",
77    "Float32Array",
78    "Float64Array",
79    // The 64-bit views store BigInt elements rather than Numbers.
80    "BigInt64Array",
81    "BigUint64Array",
82];
83
84/// Bytes per element for a typed-array kind.
85pub fn bytes_per_element(kind: &str) -> usize {
86    match kind {
87        "Int8Array" | "Uint8Array" | "Uint8ClampedArray" => 1,
88        "Int16Array" | "Uint16Array" => 2,
89        "Int32Array" | "Uint32Array" | "Float32Array" => 4,
90        "Float64Array" | "BigInt64Array" | "BigUint64Array" => 8,
91        _ => 1,
92    }
93}
94
95/// Coerce a JS number into the value stored for `kind` (integer wrap, unsigned
96/// clamp, or float), mirroring the `ToInt8`/`ToUint8Clamp`/… abstract ops.
97fn coerce(kind: &str, n: f64) -> f64 {
98    match kind {
99        "Int8Array" => (n as i64 as i8) as f64,
100        "Uint8Array" => (n as i64 as u8) as f64,
101        "Uint8ClampedArray" => {
102            if n.is_nan() {
103                0.0
104            } else {
105                n.round().clamp(0.0, 255.0)
106            }
107        }
108        "Int16Array" => (n as i64 as i16) as f64,
109        "Uint16Array" => (n as i64 as u16) as f64,
110        "Int32Array" => (n as i64 as i32) as f64,
111        "Uint32Array" => (n as i64 as u32) as f64,
112        "Float32Array" => n as f32 as f64,
113        _ => n, // Float64Array
114    }
115}
116
117/// Whether `kind` stores BigInt elements rather than Numbers. The two 64-bit
118/// views are the only ones: their elements do not fit an `f64` without loss, so
119/// the whole element pipeline carries `Value` rather than `f64`.
120pub fn is_bigint_kind(kind: &str) -> bool {
121    matches!(kind, "BigInt64Array" | "BigUint64Array")
122}
123
124/// Coerce a JS value into the element `kind` stores. The numeric kinds go
125/// through the `ToInt8`/`ToUint8Clamp`/… abstract ops as before; the 64-bit ones
126/// wrap through `ToBigInt64`/`ToBigUint64` and keep a BigInt.
127fn coerce_val(kind: &str, v: &Value) -> Result<Value, String> {
128    if !is_bigint_kind(kind) {
129        return Ok(Value::Float(coerce(kind, with_host(|h| h.to_number(v)))));
130    }
131    // 7.1.15/7.1.16: the operand must already BE a BigInt — a Number throws,
132    // which is what makes `new BigInt64Array(1)[0] = 1` a TypeError in node.
133    let big = with_host(|h| match h.get(v) {
134        Some(JsObj::BigInt(b)) => Some(b.clone()),
135        _ => None,
136    })
137    .ok_or_else(|| crate::host::type_error("Cannot convert a Number value to a BigInt"))?;
138    Ok(with_host(|h| h.new_bigint(wrap_bigint(kind, big))))
139}
140
141/// `ToBigInt64` / `ToBigUint64` — wrap modulo 2^64 into the signed or unsigned
142/// 64-bit range, which is what a 64-bit view stores.
143fn wrap_bigint(kind: &str, b: num_bigint::BigInt) -> num_bigint::BigInt {
144    use num_traits::cast::ToPrimitive;
145    let modulus = num_bigint::BigInt::from(1u128 << 64);
146    let mut m = b % &modulus;
147    if m.sign() == num_bigint::Sign::Minus {
148        m += &modulus;
149    }
150    // `m` is now in [0, 2^64); reinterpret it for the view's signedness.
151    let raw = m.to_u64().unwrap_or(0);
152    if kind == "BigInt64Array" {
153        num_bigint::BigInt::from(raw as i64)
154    } else {
155        num_bigint::BigInt::from(raw)
156    }
157}
158
159/// An element's BigInt, for ordering a 64-bit view. Zero for anything else,
160/// which the numeric kinds never ask for.
161fn bigint_of(v: &Value) -> num_bigint::BigInt {
162    with_host(|h| match h.get(v) {
163        Some(JsObj::BigInt(b)) => b.clone(),
164        _ => num_bigint::BigInt::from(0),
165    })
166}
167
168/// `indexOf`/`lastIndexOf`/`includes` element comparison. 23.2.3.x compare the
169/// search element with the STORED one and do not coerce it, so a string never
170/// matches a numeric element and a Number never matches a BigInt one.
171///
172/// `includes` differs from `indexOf` only in treating `NaN` as present
173/// (SameValueZero vs strict equality), which `nan_matches` selects: node reports
174/// `new Float64Array([NaN]).includes(NaN)` as true and `.indexOf(NaN)` as -1.
175fn same_element(stored: &Value, needle: &Value, nan_matches: bool) -> bool {
176    if nan_matches {
177        if let (Value::Float(a), Value::Float(b)) = (stored, needle) {
178            if a.is_nan() && b.is_nan() {
179                return true;
180            }
181        }
182    }
183    with_host(|h| h.strict_eq(stored, needle))
184}
185
186/// The zero element of `kind` — what a freshly allocated view is filled with.
187fn zero_of(kind: &str) -> Value {
188    if is_bigint_kind(kind) {
189        with_host(|h| h.new_bigint(num_bigint::BigInt::from(0)))
190    } else {
191        Value::Float(0.0)
192    }
193}
194
195/// An element as an `f64`, for the numeric-kind comparisons (`sort`'s default
196/// order, `indexOf`). A BigInt element answers its nearest `f64`, which is only
197/// ever used where the kind is numeric.
198fn num(v: &Value) -> f64 {
199    with_host(|h| h.to_number(v))
200}
201
202/// The element values of a typed array / Buffer as stored — `Value`, not `f64`,
203/// so a 64-bit view keeps its BigInts. `elems_of` is the numeric view of the
204/// same data and stays, because `Buffer` reads bytes through it.
205pub fn elem_values(v: &Value) -> Vec<Value> {
206    let Some(tag) = super::native_tag(v) else {
207        return Vec::new();
208    };
209    let field = match tag.as_str() {
210        "TypedArray" => "@@elems",
211        "Buffer" => "@@bytes",
212        _ => return Vec::new(),
213    };
214    with_host(|h| match h.get(v) {
215        Some(JsObj::Object(p)) => match p.get(field).and_then(|a| h.get(a)) {
216            Some(JsObj::Array(items)) => items.clone(),
217            _ => Vec::new(),
218        },
219        _ => Vec::new(),
220    })
221}
222
223/// Build a typed array of `kind` from already-coerced element values.
224fn make(kind: &str, elems: Vec<Value>) -> Value {
225    with_host(|h| {
226        let bpe = bytes_per_element(kind);
227        let len = elems.len();
228        let arr = h.new_array(elems);
229        let mut m = IndexMap::new();
230        m.insert("@@native".into(), h.new_str("TypedArray"));
231        m.insert("@@kind".into(), h.new_str(kind));
232        m.insert("@@elems".into(), arr);
233        m.insert("length".into(), Value::Float(len as f64));
234        m.insert("byteLength".into(), Value::Float((len * bpe) as f64));
235        // Every view reports where it starts in its backing store. A `Buffer`
236        // already carried this; a typed array did not, so `u8.byteOffset` read
237        // `undefined` where a Buffer read 0. Nothing here can produce a
238        // non-zero offset yet — see the note on `.buffer` below.
239        m.insert("byteOffset".into(), Value::Float(0.0));
240        m.insert("BYTES_PER_ELEMENT".into(), Value::Float(bpe as f64));
241        let obj = h.new_object(m);
242        // Link the instance to the real `Uint8Array.prototype` object so its
243        // inherited methods resolve through the chain, exactly as a `Buffer`
244        // already did. Without this a typed array was a bare tagged object and
245        // `new Uint8Array([1]).every` was not even a function — the methods
246        // existed on the prototype but nothing pointed at it.
247        h.ensure_native_protos();
248        if let Some(p) = h.native_proto(kind) {
249            h.set_proto(&obj, p);
250        }
251        // View metadata is real but non-enumerable, as it is for a Buffer.
252        for k in ["length", "byteLength", "byteOffset", "BYTES_PER_ELEMENT"] {
253            h.hide_prop(&obj, k);
254        }
255        obj
256    })
257}
258
259/// `new Uint8Array(...)` etc. `ArrayBuffer` is a byte container with only a
260/// `byteLength`.
261pub fn construct(kind: &str, args: &[Value]) -> Result<Value, String> {
262    if kind == "ArrayBuffer" {
263        let n = super::arg_num(args, 0).max(0.0) as usize;
264        return Ok(with_host(|h| {
265            let mut m = IndexMap::new();
266            m.insert("@@native".into(), h.new_str("ArrayBuffer"));
267            m.insert("byteLength".into(), Value::Float(n as f64));
268            h.new_object(m)
269        }));
270    }
271    let elems = build_elems(kind, args)?;
272    Ok(make(kind, elems))
273}
274
275/// Element vector for a typed-array construction from its first argument:
276/// a number → that many zeroed slots; an array/iterable/typed-array → its coerced
277/// values; otherwise → empty.
278fn build_elems(kind: &str, args: &[Value]) -> Result<Vec<Value>, String> {
279    match args.first() {
280        None | Some(Value::Undef) => Ok(Vec::new()),
281        Some(Value::Int(_)) | Some(Value::Float(_)) => {
282            let n = super::arg_num(args, 0).max(0.0) as usize;
283            Ok(vec![zero_of(kind); n])
284        }
285        Some(v) => {
286            // Another typed array / Buffer → copy its elements; anything else
287            // iterable → coerce each entry.
288            let items = match super::native_tag(v).as_deref() {
289                Some("TypedArray") | Some("Buffer") => elem_values(v),
290                _ => crate::host::iter_all(v).unwrap_or_default(),
291            };
292            items.iter().map(|x| coerce_val(kind, x)).collect()
293        }
294    }
295}
296
297/// `Uint8Array.from(iterable[, mapFn])` / `Uint8Array.of(...items)`.
298pub fn static_call(kind: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
299    Some(match method {
300        "of" => args
301            .iter()
302            .map(|x| coerce_val(kind, x))
303            .collect::<Result<Vec<Value>, String>>()
304            .map(|e| make(kind, e)),
305        "from" => from(kind, args),
306        // `ArrayBuffer.isView(x)` — true for a typed array or a Buffer (which is
307        // a Uint8Array view), false for the backing ArrayBuffer itself.
308        "isView" => Ok(Value::Bool(with_host(|h| {
309            matches!(
310                h.get(&args.first().cloned().unwrap_or(Value::Undef)),
311                Some(crate::host::JsObj::Object(p))
312                    if matches!(
313                        p.get("@@native").map(|t| h.str_of(t)).as_deref(),
314                        Some("TypedArray") | Some("Buffer") | Some("DataView")
315                    )
316            )
317        }))),
318        _ => return None,
319    })
320}
321
322fn from(kind: &str, args: &[Value]) -> Result<Value, String> {
323    let src = args.first().cloned().unwrap_or(Value::Undef);
324    let map_fn = args
325        .get(1)
326        .cloned()
327        .filter(|f| with_host(|h| crate::host::is_callable(h, f)));
328    let items = if let Some(e) = elems_of(&src) {
329        e.into_iter().map(Value::Float).collect()
330    } else {
331        crate::host::iter_all(&src).unwrap_or_default()
332    };
333    let mut out = Vec::with_capacity(items.len());
334    for (i, it) in items.into_iter().enumerate() {
335        let mapped = match &map_fn {
336            Some(f) => crate::host::invoke(f, vec![it, Value::Float(i as f64)], None)?,
337            None => it,
338        };
339        out.push(coerce_val(kind, &mapped)?);
340    }
341    Ok(make(kind, out))
342}
343
344/// The element values of a typed array / Buffer (`None` for anything else).
345pub fn elems_of(v: &Value) -> Option<Vec<f64>> {
346    let tag = super::native_tag(v)?;
347    let field = match tag.as_str() {
348        "TypedArray" => "@@elems",
349        "Buffer" => "@@bytes",
350        _ => return None,
351    };
352    with_host(|h| match h.get(v) {
353        Some(JsObj::Object(p)) => match p.get(field).and_then(|a| h.get(a)) {
354            Some(JsObj::Array(items)) => Some(items.iter().map(|x| h.to_number(x)).collect()),
355            _ => None,
356        },
357        _ => None,
358    })
359}
360
361/// The number of elements `v` exposes as integer-index own properties, for a
362/// typed array (`@@elems`) or a `Buffer` (`@@bytes`); `None` for anything else.
363///
364/// Both index-membership questions — `obj.hasOwnProperty(i)` and `i in obj` —
365/// must answer from this one place. They used to disagree: `hasOwnProperty`
366/// carried a hand-rolled arm that understood `@@bytes` only, so it was right for
367/// a Buffer and wrong for every other typed array, while the `in` operator knew
368/// about neither and reported false for every valid index of both.
369pub fn index_len(v: &Value) -> Option<usize> {
370    let field = match super::native_tag(v)?.as_str() {
371        "TypedArray" => "@@elems",
372        "Buffer" => "@@bytes",
373        _ => return None,
374    };
375    with_host(|h| match h.get(v) {
376        Some(JsObj::Object(p)) => match p.get(field).and_then(|a| h.get(a)) {
377            Some(JsObj::Array(items)) => Some(items.len()),
378            _ => None,
379        },
380        _ => None,
381    })
382}
383
384/// Whether `key` is an in-range integer index of the typed array / Buffer `v`.
385/// `None` when `v` is neither, so callers can fall through to their own logic.
386pub fn has_index(v: &Value, key: &str) -> Option<bool> {
387    let len = index_len(v)?;
388    Some(key.parse::<usize>().map(|i| i < len).unwrap_or(false))
389}
390
391/// The `@@kind` of a typed-array receiver (defaults to `Uint8Array`).
392pub fn kind_of(recv: &Value) -> String {
393    with_host(|h| match h.get(recv) {
394        Some(JsObj::Object(p)) => p
395            .get("@@kind")
396            .map(|v| h.str_of(v))
397            .unwrap_or_else(|| "Uint8Array".into()),
398        _ => "Uint8Array".into(),
399    })
400}
401
402// ── element indexing (called from builtins::get_property/set_property) ────────
403
404/// `ta[i]` read: the element at char/index `i`, or `None` if `i` is out of range
405/// or not an integer index.
406pub fn elem_get(recv: &Value, key: &str) -> Option<Value> {
407    let i: usize = key.parse().ok()?;
408    with_host(|h| match h.get(recv) {
409        Some(JsObj::Object(p)) => match p.get("@@elems").and_then(|a| h.get(a)) {
410            Some(JsObj::Array(items)) => items.get(i).cloned(),
411            _ => None,
412        },
413        _ => None,
414    })
415}
416
417/// `ta[i] = v` write (coerced to the kind). Returns true if `i` is a valid index.
418pub fn elem_set(recv: &Value, key: &str, val: &Value) -> Result<bool, String> {
419    let Ok(i) = key.parse::<usize>() else {
420        return Ok(false);
421    };
422    let kind = kind_of(recv);
423    // Coerced through the element type, so writing a Number into a 64-bit view
424    // throws rather than storing an un-typed element.
425    let n = coerce_val(&kind, val)?;
426    Ok(with_host(|h| {
427        if let Some(JsObj::Object(p)) = h.get(recv) {
428            if let Some(arr) = p.get("@@elems").cloned() {
429                if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
430                    if i < items.len() {
431                        items[i] = n;
432                        return true;
433                    }
434                }
435            }
436        }
437        false
438    }))
439}
440
441/// Build a result of the same "species" as `recv`: a `Buffer` receiver yields a
442/// `Buffer`, every other typed array yields its own kind. Node picks the result
443/// type from the receiver's constructor, so `Buffer.from([1]).map(f)` is a
444/// Buffer and `new Int32Array([1]).map(f)` is an `Int32Array`.
445fn species(recv: &Value, kind: &str, elems: Vec<Value>) -> Value {
446    if super::native_tag(recv).as_deref() == Some("Buffer") {
447        let bytes: Vec<u8> = elems.iter().map(|x| num(x) as i64 as u8).collect();
448        return super::buffer::from_bytes(&bytes);
449    }
450    make(kind, elems)
451}
452
453/// Overwrite `recv`'s elements in place, for the methods that mutate and return
454/// the receiver (`fill`, `reverse`, `sort`, `copyWithin`). Writes through to
455/// whichever hidden array backs it — `@@elems` for a typed array, `@@bytes` for
456/// a `Buffer`.
457fn write_elems(recv: &Value, kind: &str, vals: &[Value]) -> Result<(), String> {
458    let field = match super::native_tag(recv).as_deref() {
459        Some("Buffer") => "@@bytes",
460        _ => "@@elems",
461    };
462    // Coerce OUTSIDE the host borrow: `coerce_val` re-enters the host to read a
463    // BigInt and to allocate the wrapped one.
464    let coerced: Vec<Value> = vals
465        .iter()
466        .map(|v| coerce_val(kind, v))
467        .collect::<Result<_, _>>()?;
468    with_host(|h| {
469        if let Some(JsObj::Object(p)) = h.get(recv) {
470            if let Some(arr) = p.get(field).cloned() {
471                if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
472                    for (i, v) in coerced.into_iter().enumerate() {
473                        if i < items.len() {
474                            items[i] = v;
475                        }
476                    }
477                }
478            }
479        }
480    });
481    Ok(())
482}
483
484/// Resolve a relative index argument against `len` (negative counts from the
485/// end), clamped into range — the `RelativeIndex` coercion the typed-array
486/// methods share.
487fn rel_index(args: &[Value], idx: usize, len: usize, default: usize) -> usize {
488    if args.len() <= idx {
489        return default;
490    }
491    let n = super::arg_num(args, idx);
492    if n < 0.0 {
493        (len as f64 + n).max(0.0) as usize
494    } else {
495        (n as usize).min(len)
496    }
497}
498
499/// Typed-array instance methods.
500pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
501    let kind = kind_of(recv);
502    // Elements travel as `Value`, not `f64`: a 64-bit view's are BigInts, and
503    // rounding them through a double is exactly the loss those views exist to
504    // avoid. The numeric kinds still hold `Value::Float`, so nothing about them
505    // changes.
506    let elems = elem_values(recv);
507    // The callback-taking methods share one shape: invoke `cb(value, index,
508    // receiver)` per element. They are inherited by `Buffer` too, which is why
509    // they must live here rather than in either concrete type.
510    let call_cb = |i: usize, v: &Value| -> Result<Value, String> {
511        crate::host::invoke(
512            &args.first().cloned().unwrap_or(Value::Undef),
513            vec![v.clone(), Value::Float(i as f64), recv.clone()],
514            None,
515        )
516    };
517    match method {
518        "every" => {
519            for (i, v) in elems.iter().enumerate() {
520                let r = call_cb(i, v)?;
521                if !with_host(|h| h.truthy(&r)) {
522                    return Ok(Value::Bool(false));
523                }
524            }
525            Ok(Value::Bool(true))
526        }
527        "some" => {
528            for (i, v) in elems.iter().enumerate() {
529                let r = call_cb(i, v)?;
530                if with_host(|h| h.truthy(&r)) {
531                    return Ok(Value::Bool(true));
532                }
533            }
534            Ok(Value::Bool(false))
535        }
536        "forEach" => {
537            for (i, v) in elems.iter().enumerate() {
538                call_cb(i, v)?;
539            }
540            Ok(Value::Undef)
541        }
542        "map" => {
543            let mut out = Vec::with_capacity(elems.len());
544            for (i, v) in elems.iter().enumerate() {
545                let r = call_cb(i, v)?;
546                out.push(coerce_val(&kind, &r)?);
547            }
548            Ok(species(recv, &kind, out))
549        }
550        "filter" => {
551            let mut out = Vec::new();
552            for (i, v) in elems.iter().enumerate() {
553                let r = call_cb(i, v)?;
554                if with_host(|h| h.truthy(&r)) {
555                    out.push(v.clone());
556                }
557            }
558            Ok(species(recv, &kind, out))
559        }
560        "find" | "findIndex" | "findLast" | "findLastIndex" => {
561            let last = method.starts_with("findLast");
562            let idxs: Vec<usize> = if last {
563                (0..elems.len()).rev().collect()
564            } else {
565                (0..elems.len()).collect()
566            };
567            for i in idxs {
568                let r = call_cb(i, &elems[i])?;
569                if with_host(|h| h.truthy(&r)) {
570                    return Ok(if method.ends_with("Index") {
571                        Value::Float(i as f64)
572                    } else {
573                        elems[i].clone()
574                    });
575                }
576            }
577            Ok(if method.ends_with("Index") {
578                Value::Float(-1.0)
579            } else {
580                Value::Undef
581            })
582        }
583        "reduce" | "reduceRight" => {
584            let right = method == "reduceRight";
585            let order: Vec<usize> = if right {
586                (0..elems.len()).rev().collect()
587            } else {
588                (0..elems.len()).collect()
589            };
590            let cb = args.first().cloned().unwrap_or(Value::Undef);
591            let mut it = order.into_iter();
592            let mut acc = if args.len() >= 2 {
593                args[1].clone()
594            } else {
595                match it.next() {
596                    Some(i) => elems[i].clone(),
597                    None => {
598                        return Err(crate::host::type_error(
599                            "Reduce of empty array with no initial value",
600                        ))
601                    }
602                }
603            };
604            for i in it {
605                acc = crate::host::invoke(
606                    &cb,
607                    vec![acc, elems[i].clone(), Value::Float(i as f64), recv.clone()],
608                    None,
609                )?;
610            }
611            Ok(acc)
612        }
613        "reverse" => {
614            let mut out = elems.clone();
615            out.reverse();
616            write_elems(recv, &kind, &out)?;
617            Ok(recv.clone())
618        }
619        "sort" => {
620            let mut out = elems.clone();
621            let cmp = args.first().cloned().unwrap_or(Value::Undef);
622            if with_host(|h| crate::host::is_callable(h, &cmp)) {
623                // A user comparator goes through the same fallible merge sort
624                // `Array.prototype.sort` uses: O(n log n) rather than the
625                // insertion sort this was, and a comparator returning NaN keeps
626                // the pair's order (23.2.4.1 step 3: NaN is +0) instead of
627                // swapping, which the `<= 0.0` break got wrong.
628                crate::builtins::sort_values(&mut out, Some(&cmp))?;
629            } else {
630                // A typed array sorts NUMERICALLY by default, unlike `Array`
631                // which sorts by string. Verified against node v26.7.0:
632                // `new Uint8Array([10,9,1]).sort()` is `1,9,10` while
633                // `[10,9,1].sort()` is `1,10,9`.
634                // A BigInt element cannot be ordered through an `f64` without
635                // collapsing values more than 2^53 apart, so the 64-bit views
636                // compare the integers themselves.
637                if is_bigint_kind(&kind) {
638                    let keys: Vec<num_bigint::BigInt> = out.iter().map(bigint_of).collect();
639                    let mut idx: Vec<usize> = (0..out.len()).collect();
640                    idx.sort_by(|a, b| keys[*a].cmp(&keys[*b]));
641                    out = idx.into_iter().map(|i| out[i].clone()).collect();
642                } else {
643                    out.sort_by(|a, b| {
644                        num(a)
645                            .partial_cmp(&num(b))
646                            .unwrap_or(std::cmp::Ordering::Equal)
647                    });
648                }
649            }
650            write_elems(recv, &kind, &out)?;
651            Ok(recv.clone())
652        }
653        "copyWithin" => {
654            let len = elems.len();
655            let target = rel_index(args, 0, len, 0);
656            let start = rel_index(args, 1, len, 0);
657            let end = rel_index(args, 2, len, len);
658            let src: Vec<Value> = elems[start.min(end)..end.max(start)].to_vec();
659            let mut out = elems.clone();
660            for (k, v) in src.iter().enumerate() {
661                if target + k < len {
662                    out[target + k] = v.clone();
663                }
664            }
665            write_elems(recv, &kind, &out)?;
666            Ok(recv.clone())
667        }
668        "at" => {
669            let n = super::arg_num(args, 0);
670            let i = if n < 0.0 { elems.len() as f64 + n } else { n };
671            if i < 0.0 || i >= elems.len() as f64 {
672                return Ok(Value::Undef);
673            }
674            Ok(elems[i as usize].clone())
675        }
676        "lastIndexOf" => {
677            let needle = args.first().cloned().unwrap_or(Value::Undef);
678            Ok(Value::Float(
679                elems
680                    .iter()
681                    .rposition(|x| same_element(x, &needle, false))
682                    .map(|p| p as f64)
683                    .unwrap_or(-1.0),
684            ))
685        }
686        "keys" | "values" | "entries" => {
687            let items: Vec<Value> = with_host(|h| match method {
688                "keys" => (0..elems.len()).map(|i| Value::Float(i as f64)).collect(),
689                "values" => elems.clone(),
690                _ => elems
691                    .iter()
692                    .enumerate()
693                    .map(|(i, v)| h.new_array(vec![Value::Float(i as f64), v.clone()]))
694                    .collect(),
695            });
696            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
697        }
698        "toString" | "join" => {
699            let sep = if method == "join" && !args.is_empty() {
700                super::arg_str(args, 0)
701            } else {
702                ",".into()
703            };
704            let parts: Vec<String> = with_host(|h| elems.iter().map(|n| h.str_of(n)).collect());
705            Ok(with_host(|h| h.new_str(parts.join(&sep))))
706        }
707        "slice" | "subarray" => {
708            let len = elems.len();
709            let norm = |n: f64| -> usize {
710                if n < 0.0 {
711                    (len as f64 + n).max(0.0) as usize
712                } else {
713                    (n as usize).min(len)
714                }
715            };
716            let s = if args.is_empty() {
717                0
718            } else {
719                norm(super::arg_num(args, 0))
720            };
721            let e = if args.len() < 2 {
722                len
723            } else {
724                norm(super::arg_num(args, 1))
725            };
726            Ok(make(&kind, elems[s.min(e)..e.max(s)].to_vec()))
727        }
728        "indexOf" => {
729            let needle = args.first().cloned().unwrap_or(Value::Undef);
730            Ok(Value::Float(
731                elems
732                    .iter()
733                    .position(|x| same_element(x, &needle, false))
734                    .map(|p| p as f64)
735                    .unwrap_or(-1.0),
736            ))
737        }
738        "includes" => {
739            let needle = args.first().cloned().unwrap_or(Value::Undef);
740            Ok(Value::Bool(
741                elems.iter().any(|x| same_element(x, &needle, true)),
742            ))
743        }
744        "fill" => {
745            let v = coerce_val(&kind, args.first().unwrap_or(&Value::Undef))?;
746            Ok(make(&kind, vec![v; elems.len()]))
747        }
748        "set" => {
749            // `ta.set(src[, offset])` — write `src`'s values in place.
750            let arg = args.first().cloned().unwrap_or(Value::Undef);
751            let src = match super::native_tag(&arg).as_deref() {
752                Some("TypedArray") | Some("Buffer") => elem_values(&arg),
753                _ => crate::host::iter_all(&arg).unwrap_or_default(),
754            };
755            let off = super::arg_num(args, 1).max(0.0) as usize;
756            // Coerced outside the host borrow: a 64-bit element allocates.
757            let src: Vec<Value> = src
758                .iter()
759                .map(|v| coerce_val(&kind, v))
760                .collect::<Result<_, _>>()?;
761            with_host(|h| {
762                if let Some(JsObj::Object(p)) = h.get(recv) {
763                    if let Some(arr) = p.get("@@elems").cloned() {
764                        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
765                            for (k, v) in src.into_iter().enumerate() {
766                                if off + k < items.len() {
767                                    items[off + k] = v;
768                                }
769                            }
770                        }
771                    }
772                }
773            });
774            Ok(Value::Undef)
775        }
776        _ => Err(crate::host::type_error(&format!(
777            "{method} is not a function"
778        ))),
779    }
780}
781
782// ── WeakRef (strong-ref approximation) ────────────────────────────────────────
783
784pub fn construct_weakref(args: &[Value]) -> Result<Value, String> {
785    let target = args.first().cloned().unwrap_or(Value::Undef);
786    Ok(with_host(|h| {
787        let mut m = IndexMap::new();
788        m.insert("@@native".into(), h.new_str("WeakRef"));
789        m.insert("@@target".into(), target);
790        h.new_object(m)
791    }))
792}
793
794pub fn weakref_call(recv: &Value, method: &str) -> Result<Value, String> {
795    match method {
796        "deref" => Ok(with_host(|h| match h.get(recv) {
797            Some(JsObj::Object(p)) => p.get("@@target").cloned().unwrap_or(Value::Undef),
798            _ => Value::Undef,
799        })),
800        _ => Err(crate::host::type_error(&format!(
801            "{method} is not a function"
802        ))),
803    }
804}
805
806// ── FinalizationRegistry (no-GC approximation) ────────────────────────────────
807//
808// This VM holds every value strongly (see `WeakRef` above), so a registered
809// target is never reclaimed and the cleanup callback never fires. The ECMAScript
810// spec permits an implementation to never call cleanup callbacks, so this is a
811// conformant approximation: the constructor and `register`/`unregister` enforce
812// their type checks and `unregister`'s bookkeeping exactly, only the (optional)
813// callback invocation is absent. Registered unregister-tokens are tracked in a
814// hidden `@@fr_tokens` array so `unregister` returns the correct boolean.
815
816/// Whether `v` is an Object (a valid `register` target / unregister token) — a
817/// heap value that is not one of the primitive-wrapper heap variants.
818fn is_object_value(v: &Value) -> bool {
819    matches!(v, Value::Obj(_))
820        && with_host(|h| {
821            !matches!(
822                h.get(v),
823                Some(JsObj::Str(_))
824                    | Some(JsObj::Symbol { .. })
825                    | Some(JsObj::BigInt(_))
826                    | Some(JsObj::Null)
827            )
828        })
829}
830
831pub fn construct_finalization_registry(args: &[Value]) -> Result<Value, String> {
832    let cb = args.first().cloned().unwrap_or(Value::Undef);
833    if !with_host(|h| crate::host::is_callable(h, &cb)) {
834        return Err(crate::host::type_error(
835            "FinalizationRegistry: cleanup must be callable",
836        ));
837    }
838    Ok(with_host(|h| {
839        let tokens = h.new_array(Vec::new());
840        let mut m = IndexMap::new();
841        m.insert("@@native".into(), h.new_str("FinalizationRegistry"));
842        m.insert("@@fr_cb".into(), cb);
843        m.insert("@@fr_tokens".into(), tokens);
844        h.new_object(m)
845    }))
846}
847
848pub fn finalization_registry_call(
849    recv: &Value,
850    method: &str,
851    args: &[Value],
852) -> Result<Value, String> {
853    match method {
854        "register" => {
855            let target = args.first().cloned().unwrap_or(Value::Undef);
856            let held = args.get(1).cloned().unwrap_or(Value::Undef);
857            let token = args.get(2).cloned().unwrap_or(Value::Undef);
858            if !is_object_value(&target) {
859                // V8's wording is `invalid target`; the "must be an object"
860                // phrasing was this file's own, not any engine's.
861                return Err(crate::host::type_error(
862                    "FinalizationRegistry.prototype.register: invalid target",
863                ));
864            }
865            if with_host(|h| h.strict_eq(&target, &held)) {
866                return Err(crate::host::type_error(
867                    "FinalizationRegistry.prototype.register: target and holdings must not be same",
868                ));
869            }
870            // A supplied unregister token must be an object; record it so a later
871            // `unregister` can find (and drop) this registration.
872            if !matches!(token, Value::Undef) {
873                if !is_object_value(&token) {
874                    return Err(crate::host::type_error(&format!(
875                        "Invalid unregisterToken ('{}')",
876                        with_host(|h| h.str_of(&token))
877                    )));
878                }
879                with_host(|h| {
880                    let toks = registry_tokens(h, recv);
881                    if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
882                        items.push(token);
883                    }
884                });
885            }
886            Ok(Value::Undef)
887        }
888        "unregister" => {
889            let token = args.first().cloned().unwrap_or(Value::Undef);
890            if !is_object_value(&token) {
891                // V8 names the token and does not mention the method.
892                return Err(crate::host::type_error(&format!(
893                    "Invalid unregisterToken ('{}')",
894                    with_host(|h| h.str_of(&token))
895                )));
896            }
897            Ok(Value::Bool(with_host(|h| {
898                let toks = registry_tokens(h, recv);
899                let kept: Vec<Value> = match h.get(&toks) {
900                    Some(JsObj::Array(items)) => items
901                        .iter()
902                        .filter(|t| !h.strict_eq(t, &token))
903                        .cloned()
904                        .collect(),
905                    _ => Vec::new(),
906                };
907                let removed = match h.get(&toks) {
908                    Some(JsObj::Array(items)) => items.len() != kept.len(),
909                    _ => false,
910                };
911                if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
912                    *items = kept;
913                }
914                removed
915            })))
916        }
917        _ => Err(crate::host::type_error(&format!(
918            "{method} is not a function"
919        ))),
920    }
921}
922
923/// The hidden `@@fr_tokens` array backing a `FinalizationRegistry`.
924fn registry_tokens(h: &crate::host::JsHost, recv: &Value) -> Value {
925    match h.get(recv) {
926        Some(JsObj::Object(p)) => p.get("@@fr_tokens").cloned().unwrap_or(Value::Undef),
927        _ => Value::Undef,
928    }
929}
930
931// ── TextEncoder / TextDecoder ─────────────────────────────────────────────────
932
933pub fn construct_text_encoder() -> Result<Value, String> {
934    Ok(with_host(|h| {
935        let mut m = IndexMap::new();
936        m.insert("@@native".into(), h.new_str("TextEncoder"));
937        m.insert("encoding".into(), h.new_str("utf-8"));
938        h.new_object(m)
939    }))
940}
941
942pub fn text_encoder_call(_recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
943    match method {
944        // `encode(str)` → a Uint8Array of the UTF-8 bytes.
945        "encode" => {
946            let s = super::arg_str(args, 0);
947            Ok(make(
948                "Uint8Array",
949                s.as_bytes()
950                    .iter()
951                    .map(|b| Value::Float(*b as f64))
952                    .collect(),
953            ))
954        }
955        _ => Err(crate::host::type_error(&format!(
956            "{method} is not a function"
957        ))),
958    }
959}
960
961pub fn construct_text_decoder(args: &[Value]) -> Result<Value, String> {
962    let label = if args.is_empty() {
963        "utf-8".to_string()
964    } else {
965        super::arg_str(args, 0)
966    };
967    Ok(with_host(|h| {
968        let mut m = IndexMap::new();
969        m.insert("@@native".into(), h.new_str("TextDecoder"));
970        m.insert("encoding".into(), h.new_str(label.to_ascii_lowercase()));
971        h.new_object(m)
972    }))
973}
974
975pub fn text_decoder_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
976    match method {
977        // `decode(bytes)` → a string from the buffer's UTF-8 (or latin1) bytes.
978        "decode" => {
979            let bytes: Vec<u8> = elems_of(&args.first().cloned().unwrap_or(Value::Undef))
980                .unwrap_or_default()
981                .iter()
982                .map(|n| *n as u8)
983                .collect();
984            let enc = with_host(|h| match h.get(recv) {
985                Some(JsObj::Object(p)) => p
986                    .get("encoding")
987                    .map(|v| h.str_of(v))
988                    .unwrap_or_else(|| "utf-8".into()),
989                _ => "utf-8".into(),
990            });
991            let s = match enc.as_str() {
992                "latin1" | "iso-8859-1" | "ascii" => bytes.iter().map(|b| *b as char).collect(),
993                _ => String::from_utf8_lossy(&bytes).into_owned(),
994            };
995            Ok(with_host(|h| h.new_str(s)))
996        }
997        _ => Err(crate::host::type_error(&format!(
998            "{method} is not a function"
999        ))),
1000    }
1001}