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`), a window (`@@buffer`/`byteOffset`/`length`) onto the bytes
6//! its `ArrayBuffer` owns, and the
7//! enumerable `length`/`byteLength`/`BYTES_PER_ELEMENT` data properties JS code
8//! reads directly. Element indexing (`ta[i]` get/set) is special-cased in
9//! `builtins::get_property`/`set_property` via `elem_get`/`elem_set` here, which
10//! also apply each kind's coercion (integer wrap / clamp / float).
11//!
12//! `WeakRef` holds a *strong* reference (`deref()` always returns the target) —
13//! node-js has no GC of JS objects, so this is observably correct for the
14//! express dependency tree (object-inspect/qs/side-channel only ever `deref()`).
15
16use crate::host::{fmt_number, with_host, JsObj};
17use fusevm::Value;
18use indexmap::IndexMap;
19
20pub const STATIC_METHODS: &[&str] = &["from", "of", "isView"];
21
22/// `Uint8Array`'s statics — the shared three plus the base64/hex pair, which no
23/// other view has.
24pub const UINT8_STATIC_METHODS: &[&str] = &["from", "of", "isView", "fromBase64", "fromHex"];
25
26/// The four base64/hex methods `Uint8Array.prototype` owns on its own.
27/// In the engine's own order, which `Object.getOwnPropertyNames` reports.
28pub const UINT8_PROTOTYPE_METHODS: &[&str] = &["toBase64", "setFromBase64", "toHex", "setFromHex"];
29
30/// The statics `<kind>` advertises.
31pub fn static_methods(kind: &str) -> &'static [&'static str] {
32    if kind == "Uint8Array" {
33        UINT8_STATIC_METHODS
34    } else {
35        STATIC_METHODS
36    }
37}
38
39/// The methods installed on the real `Uint8Array.prototype` object (as
40/// `@proto:Uint8Array:<m>` thunks), so `Uint8Array.prototype.slice.call(x)`
41/// keeps working now that the prototype is an object rather than a `Builtin`
42/// namespace whose every property read synthesized a thunk.
43pub const PROTOTYPE_METHODS: &[&str] = &[
44    "at",
45    "copyWithin",
46    "entries",
47    "every",
48    "fill",
49    "filter",
50    "find",
51    "findIndex",
52    "findLast",
53    "findLastIndex",
54    "forEach",
55    "includes",
56    "indexOf",
57    "join",
58    "keys",
59    "lastIndexOf",
60    "map",
61    "reduce",
62    "reduceRight",
63    "reverse",
64    "set",
65    "slice",
66    "some",
67    "sort",
68    "subarray",
69    "toReversed",
70    "toSorted",
71    "toString",
72    "values",
73    "with",
74];
75
76/// The eleven element kinds plus the two buffer types.
77pub fn is_ctor(name: &str) -> bool {
78    ELEMENT_KINDS.contains(&name) || matches!(name, "ArrayBuffer" | "DataView")
79}
80
81/// The element kinds, each of which gets its own real prototype object whose
82/// parent is the shared `%TypedArray%.prototype`. `Uint8Array` leads because
83/// `Buffer.prototype` chains onto it.
84///
85/// `BigInt64Array`/`BigUint64Array` are here too, and they are not
86/// interchangeable with the rest: their elements are BigInts, so a Number
87/// written into one is a `TypeError` and a `Number`-kind view will not accept
88/// one either (`coerce_val`).
89pub const ELEMENT_KINDS: &[&str] = &[
90    "Uint8Array",
91    "Int8Array",
92    "Uint8ClampedArray",
93    "Int16Array",
94    "Uint16Array",
95    "Int32Array",
96    "Uint32Array",
97    "Float32Array",
98    "Float64Array",
99    // The 64-bit views store BigInt elements rather than Numbers.
100    "BigInt64Array",
101    "BigUint64Array",
102];
103
104/// Bytes per element for a typed-array kind.
105pub fn bytes_per_element(kind: &str) -> usize {
106    match kind {
107        "Int8Array" | "Uint8Array" | "Uint8ClampedArray" => 1,
108        "Int16Array" | "Uint16Array" => 2,
109        "Int32Array" | "Uint32Array" | "Float32Array" => 4,
110        "Float64Array" | "BigInt64Array" | "BigUint64Array" => 8,
111        _ => 1,
112    }
113}
114
115/// Coerce a JS number into the value stored for `kind` (integer wrap, unsigned
116/// clamp, or float), mirroring the `ToInt8`/`ToUint8Clamp`/… abstract ops.
117fn coerce(kind: &str, n: f64) -> f64 {
118    match kind {
119        "Int8Array" => (n as i64 as i8) as f64,
120        "Uint8Array" => (n as i64 as u8) as f64,
121        "Uint8ClampedArray" => {
122            if n.is_nan() {
123                0.0
124            } else {
125                n.round().clamp(0.0, 255.0)
126            }
127        }
128        "Int16Array" => (n as i64 as i16) as f64,
129        "Uint16Array" => (n as i64 as u16) as f64,
130        "Int32Array" => (n as i64 as i32) as f64,
131        "Uint32Array" => (n as i64 as u32) as f64,
132        "Float32Array" => n as f32 as f64,
133        _ => n, // Float64Array
134    }
135}
136
137/// Whether `kind` stores BigInt elements rather than Numbers. The two 64-bit
138/// views are the only ones: their elements do not fit an `f64` without loss, so
139/// the whole element pipeline carries `Value` rather than `f64`.
140pub fn is_bigint_kind(kind: &str) -> bool {
141    matches!(kind, "BigInt64Array" | "BigUint64Array")
142}
143
144/// Coerce a JS value into the element `kind` stores. The numeric kinds go
145/// through the `ToInt8`/`ToUint8Clamp`/… abstract ops as before; the 64-bit ones
146/// wrap through `ToBigInt64`/`ToBigUint64` and keep a BigInt.
147fn coerce_val(kind: &str, v: &Value) -> Result<Value, String> {
148    if !is_bigint_kind(kind) {
149        return Ok(Value::Float(coerce(kind, with_host(|h| h.to_number(v)))));
150    }
151    // 7.1.15/7.1.16 route through `ToBigInt`, which is not "must already be a
152    // BigInt": a boolean, a string and any object that converts to one are all
153    // accepted (`a[0] = '12'` stores `12n`), and only a Number is refused. The
154    // check here was the identity test, so it rejected every one of those and
155    // reported the same wrong text — node names the value it could not convert.
156    let big = crate::builtins::to_bigint(v)?;
157    Ok(with_host(|h| h.new_bigint(wrap_bigint(kind, big))))
158}
159
160/// `ToBigInt64` / `ToBigUint64` — wrap modulo 2^64 into the signed or unsigned
161/// 64-bit range, which is what a 64-bit view stores.
162fn wrap_bigint(kind: &str, b: num_bigint::BigInt) -> num_bigint::BigInt {
163    use num_traits::cast::ToPrimitive;
164    let modulus = num_bigint::BigInt::from(1u128 << 64);
165    let mut m = b % &modulus;
166    if m.sign() == num_bigint::Sign::Minus {
167        m += &modulus;
168    }
169    // `m` is now in [0, 2^64); reinterpret it for the view's signedness.
170    let raw = m.to_u64().unwrap_or(0);
171    if kind == "BigInt64Array" {
172        num_bigint::BigInt::from(raw as i64)
173    } else {
174        num_bigint::BigInt::from(raw)
175    }
176}
177
178/// An element's BigInt, for ordering a 64-bit view. Zero for anything else,
179/// which the numeric kinds never ask for.
180fn bigint_of(v: &Value) -> num_bigint::BigInt {
181    with_host(|h| match h.get(v) {
182        Some(JsObj::BigInt(b)) => b.clone(),
183        _ => num_bigint::BigInt::from(0),
184    })
185}
186
187/// `indexOf`/`lastIndexOf`/`includes` element comparison. 23.2.3.x compare the
188/// search element with the STORED one and do not coerce it, so a string never
189/// matches a numeric element and a Number never matches a BigInt one.
190///
191/// `includes` differs from `indexOf` only in treating `NaN` as present
192/// (SameValueZero vs strict equality), which `nan_matches` selects: node reports
193/// `new Float64Array([NaN]).includes(NaN)` as true and `.indexOf(NaN)` as -1.
194fn same_element(stored: &Value, needle: &Value, nan_matches: bool) -> bool {
195    if nan_matches {
196        if let (Value::Float(a), Value::Float(b)) = (stored, needle) {
197            if a.is_nan() && b.is_nan() {
198                return true;
199            }
200        }
201    }
202    with_host(|h| h.strict_eq(stored, needle))
203}
204
205/// The zero element of `kind` — what a freshly allocated view is filled with.
206fn zero_of(kind: &str) -> Value {
207    if is_bigint_kind(kind) {
208        with_host(|h| h.new_bigint(num_bigint::BigInt::from(0)))
209    } else {
210        Value::Float(0.0)
211    }
212}
213
214/// An element as an `f64`, for the numeric-kind comparisons (`sort`'s default
215/// order, `indexOf`). A BigInt element answers its nearest `f64`, which is only
216/// ever used where the kind is numeric.
217fn num(v: &Value) -> f64 {
218    with_host(|h| h.to_number(v))
219}
220
221/// The element values of a typed array / Buffer as stored — `Value`, not `f64`,
222/// so a 64-bit view keeps its BigInts. `elems_of` is the numeric view of the
223/// same data and stays, because `Buffer` reads bytes through it.
224pub fn elem_values(v: &Value) -> Vec<Value> {
225    let Some(tag) = super::native_tag(v) else {
226        return Vec::new();
227    };
228    if tag == "TypedArray" {
229        let kind = kind_of(v);
230        let bpe = bytes_per_element(&kind);
231        return (0..view_len(v))
232            .map(|i| {
233                view_bytes(v, i * bpe, bpe)
234                    .map(|b| decode(&kind, &b))
235                    .unwrap_or(Value::Undef)
236            })
237            .collect();
238    }
239    if tag != "Buffer" {
240        return Vec::new();
241    }
242    with_host(|h| match h.get(v) {
243        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
244            Some(JsObj::Array(items)) => items.clone(),
245            _ => Vec::new(),
246        },
247        _ => Vec::new(),
248    })
249}
250
251/// Build a typed array of `kind` from already-coerced element values.
252fn make(kind: &str, elems: Vec<Value>) -> Value {
253    let bpe = bytes_per_element(kind);
254    let len = elems.len();
255    let buf = new_array_buffer(len * bpe);
256    let view = make_view(kind, &buf, 0, len);
257    for (i, e) in elems.iter().enumerate() {
258        write_view_bytes(&view, i * bpe, &encode(kind, e));
259    }
260    view
261}
262
263/// A typed array of `kind` over `buf`, `len` elements from `byte_off`.
264fn make_view(kind: &str, buf: &Value, byte_off: usize, len: usize) -> Value {
265    with_host(|h| {
266        let bpe = bytes_per_element(kind);
267        let mut m = IndexMap::new();
268        m.insert("@@native".into(), h.new_str("TypedArray"));
269        m.insert("@@kind".into(), h.new_str(kind));
270        m.insert("@@buffer".into(), buf.clone());
271        // `ta.buffer` is a non-enumerable accessor in node; a hidden own slot
272        // reads identically and keeps it out of `Object.keys` and `inspect`.
273        m.insert("buffer".into(), buf.clone());
274        m.insert("length".into(), Value::Float(len as f64));
275        m.insert("byteLength".into(), Value::Float((len * bpe) as f64));
276        // Every view reports where it starts in its backing store. A `Buffer`
277        // already carried this; a typed array did not, so `u8.byteOffset` read
278        // `undefined` where a Buffer read 0. Nothing here can produce a
279        // non-zero offset yet — see the note on `.buffer` below.
280        m.insert("byteOffset".into(), Value::Float(byte_off as f64));
281        m.insert("BYTES_PER_ELEMENT".into(), Value::Float(bpe as f64));
282        let obj = h.new_object(m);
283        // Link the instance to the real `Uint8Array.prototype` object so its
284        // inherited methods resolve through the chain, exactly as a `Buffer`
285        // already did. Without this a typed array was a bare tagged object and
286        // `new Uint8Array([1]).every` was not even a function — the methods
287        // existed on the prototype but nothing pointed at it.
288        h.ensure_native_protos();
289        if let Some(p) = h.native_proto(kind) {
290            h.set_proto(&obj, p);
291        }
292        // View metadata is real but non-enumerable, as it is for a Buffer.
293        for k in [
294            "buffer",
295            "length",
296            "byteLength",
297            "byteOffset",
298            "BYTES_PER_ELEMENT",
299        ] {
300            h.hide_prop(&obj, k);
301        }
302        obj
303    })
304}
305
306/// ToIntegerOrInfinity (7.1.5): truncation, with NaN as 0.
307fn integer_or_infinity(n: f64) -> f64 {
308    if n.is_nan() {
309        0.0
310    } else {
311        n.trunc() + 0.0
312    }
313}
314
315/// ToIndex (7.1.22): `None` for a negative or above-2^53-1 integer.
316fn to_index(n: f64) -> Option<usize> {
317    let i = integer_or_infinity(n);
318    (0.0..=9_007_199_254_740_991.0)
319        .contains(&i)
320        .then_some(i as usize)
321}
322
323/// Whether a constructor argument is a primitive rather than an Object.
324fn is_primitive(v: &Value) -> bool {
325    match v {
326        Value::Obj(_) => with_host(|h| {
327            matches!(
328                h.get(v),
329                Some(JsObj::Str(_))
330                    | Some(JsObj::Null)
331                    | Some(JsObj::BigInt(_))
332                    | Some(JsObj::Symbol { .. })
333            )
334        }),
335        _ => true,
336    }
337}
338
339/// `new Uint8Array(...)` etc. `ArrayBuffer` is a byte container with only a
340/// `byteLength`.
341pub fn construct(kind: &str, args: &[Value]) -> Result<Value, String> {
342    if kind == "ArrayBuffer" {
343        let n = to_index(super::arg_num(args, 0))
344            .ok_or_else(|| crate::host::range_error("Invalid array buffer length"))?;
345        // `maxByteLength` is read (and validated) before the buffer exists.
346        let max = match args.get(1) {
347            Some(opts) => {
348                crate::builtins::get_property(opts, "maxByteLength").unwrap_or(Value::Undef)
349            }
350            None => Value::Undef,
351        };
352        let max_len = match max {
353            Value::Undef => None,
354            _ => match to_index(with_host(|h| h.to_number(&max))) {
355                Some(m) if m >= n => Some(m),
356                _ => return Err(crate::host::range_error("Invalid array buffer max length")),
357            },
358        };
359        let ab = new_array_buffer(n);
360        // `new ArrayBuffer(n, { maxByteLength })` is a RESIZABLE buffer, which
361        // reports `resizable` and `maxByteLength` and accepts `resize`.
362        if let Some(m) = max_len {
363            with_host(|h| {
364                if let Some(JsObj::Object(p)) = h.get_mut(&ab) {
365                    p.insert("@@maxByteLength".into(), Value::Float(m as f64));
366                    p.insert("maxByteLength".into(), Value::Float(m as f64));
367                    p.insert("resizable".into(), Value::Bool(true));
368                }
369                h.hide_prop(&ab, "maxByteLength");
370                h.hide_prop(&ab, "resizable");
371            });
372        }
373        return Ok(ab);
374    }
375    // `new Uint8Array(buffer[, byteOffset[, length]])` — a VIEW onto an existing
376    // buffer rather than a fresh copy. This is the form that makes two views
377    // alias, and it did not exist: the argument fell through to the iterable
378    // branch and produced an empty array.
379    if let Some(first) = args.first() {
380        if super::native_tag(first).as_deref() == Some("ArrayBuffer") {
381            // A DETACHED buffer has no bytes to view.
382            if is_detached(first) {
383                return Err(crate::host::type_error(
384                    "Cannot perform Construct on a detached ArrayBuffer",
385                ));
386            }
387            // InitializeTypedArrayFromArrayBuffer (23.2.5.1.3), in its order,
388            // with V8's messages: they name the offending number as given.
389            let bpe = bytes_per_element(kind);
390            let total = buffer_byte_length(first);
391            let off_n = super::arg_num(args, 1);
392            let off = to_index(off_n).ok_or_else(|| {
393                crate::host::range_error(&format!(
394                    "Start offset {} is outside the bounds of the buffer",
395                    fmt_number(off_n)
396                ))
397            })?;
398            if off % bpe != 0 {
399                return Err(crate::host::range_error(&format!(
400                    "start offset of {kind} should be a multiple of {bpe}"
401                )));
402            }
403            let len = match args.get(2) {
404                Some(Value::Undef) | None => {
405                    if total % bpe != 0 {
406                        return Err(crate::host::range_error(&format!(
407                            "byte length of {kind} should be a multiple of {bpe}"
408                        )));
409                    }
410                    if off > total {
411                        return Err(crate::host::range_error(&format!(
412                            "Start offset {off} is outside the bounds of the buffer"
413                        )));
414                    }
415                    (total - off) / bpe
416                }
417                Some(_) => {
418                    let len_n = super::arg_num(args, 2);
419                    let bad = || {
420                        crate::host::range_error(&format!(
421                            "Invalid typed array length: {}",
422                            fmt_number(len_n)
423                        ))
424                    };
425                    let len = to_index(len_n).ok_or_else(bad)?;
426                    if off + len * bpe > total {
427                        return Err(bad());
428                    }
429                    len
430                }
431            };
432            return Ok(make_view(kind, first, off, len));
433        }
434    }
435    let elems = build_elems(kind, args)?;
436    Ok(make(kind, elems))
437}
438
439/// The methods a `DataView` instance exposes.
440pub const DATAVIEW_METHODS: &[&str] = &[
441    "getInt8",
442    "getUint8",
443    "getInt16",
444    "getUint16",
445    "getInt32",
446    "getUint32",
447    "getFloat32",
448    "getFloat64",
449    "getBigInt64",
450    "getBigUint64",
451    "setInt8",
452    "setUint8",
453    "setInt16",
454    "setUint16",
455    "setInt32",
456    "setUint32",
457    "setFloat32",
458    "setFloat64",
459    "setBigInt64",
460    "setBigUint64",
461];
462
463/// `new DataView(buffer[, byteOffset[, byteLength]])`.
464pub fn construct_dataview(args: &[Value]) -> Result<Value, String> {
465    let buf = args.first().cloned().unwrap_or(Value::Undef);
466    if super::native_tag(&buf).as_deref() != Some("ArrayBuffer") {
467        return Err(crate::host::type_error(
468            "First argument to DataView constructor must be an ArrayBuffer",
469        ));
470    }
471    // 25.3.2.1, with V8's messages, which name the offending value after
472    // ToIntegerOrInfinity (`-1.5` reports as `-1`).
473    let total = buffer_byte_length(&buf);
474    let off_n = super::arg_num(args, 1);
475    let outside = |n: f64| {
476        crate::host::range_error(&format!(
477            "Start offset {} is outside the bounds of the buffer",
478            fmt_number(integer_or_infinity(n))
479        ))
480    };
481    let off = to_index(off_n).ok_or_else(|| outside(off_n))?;
482    if off > total {
483        return Err(outside(off_n));
484    }
485    let bad_len = |n: f64| {
486        crate::host::range_error(&format!(
487            "Invalid DataView length {}",
488            fmt_number(integer_or_infinity(n))
489        ))
490    };
491    let len = match args.get(2) {
492        Some(Value::Undef) | None => total - off,
493        Some(_) => {
494            let len_n = super::arg_num(args, 2);
495            let len = to_index(len_n).ok_or_else(|| bad_len(len_n))?;
496            if off + len > total {
497                return Err(bad_len(len_n));
498            }
499            len
500        }
501    };
502    Ok(with_host(|h| {
503        let mut m = IndexMap::new();
504        m.insert("@@native".into(), h.new_str("DataView"));
505        m.insert("@@buffer".into(), buf.clone());
506        m.insert("buffer".into(), buf.clone());
507        m.insert("byteOffset".into(), Value::Float(off as f64));
508        m.insert("byteLength".into(), Value::Float(len as f64));
509        let obj = h.new_object(m);
510        for k in ["buffer", "byteOffset", "byteLength"] {
511            h.hide_prop(&obj, k);
512        }
513        h.ensure_native_protos();
514        if let Some(p) = h.ensure_ctor_proto("DataView") {
515            h.set_proto(&obj, p);
516        }
517        obj
518    }))
519}
520
521/// `dv.getUint16(off[, littleEndian])` and its siblings. A `DataView` defaults
522/// to BIG-endian, unlike a typed array, which is the whole reason it exists.
523pub fn dataview_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
524    if view_detached(recv) {
525        return Err(detached_error("DataView.prototype", method, false));
526    }
527    let Some(spec) = method.get(3..) else {
528        return Err(crate::host::type_error(&format!(
529            "{method} is not a function"
530        )));
531    };
532    let width = match spec {
533        "Int8" | "Uint8" => 1,
534        "Int16" | "Uint16" => 2,
535        "Int32" | "Uint32" | "Float32" => 4,
536        "Float64" | "BigInt64" | "BigUint64" => 8,
537        _ => {
538            return Err(crate::host::type_error(&format!(
539                "{method} is not a function"
540            )))
541        }
542    };
543    let is_get = method.starts_with("get");
544    // `ToIndex(requestIndex)` (25.3.1.1 step 3): NaN is 0 and a fraction
545    // truncates toward zero, so `dv.getUint8(1.9)` reads index 1. A NEGATIVE
546    // index was being clamped to 0 — `dv.getUint8(-2)` quietly read the first
547    // byte where node reports the out-of-bounds RangeError.
548    let requested = super::arg_num(args, 0);
549    let requested = if requested.is_nan() {
550        0.0
551    } else {
552        requested.trunc()
553    };
554    let span = with_host(|h| match h.get(recv) {
555        Some(JsObj::Object(p)) => p.get("byteLength").map(|l| h.to_number(l)).unwrap_or(0.0),
556        _ => 0.0,
557    });
558    if requested < 0.0 || requested + width as f64 > span {
559        return Err(crate::host::range_error(
560            "Offset is outside the bounds of the DataView",
561        ));
562    }
563    let at = requested as usize;
564    // The endianness flag is the LAST argument, and it is the second for a
565    // getter but the third for a setter.
566    let le = with_host(|h| {
567        h.truthy(
568            args.get(if is_get { 1 } else { 2 })
569                .unwrap_or(&Value::Undef),
570        )
571    });
572    if is_get {
573        let mut b = view_bytes(recv, at, width).unwrap_or_else(|| vec![0; width]);
574        if !le {
575            b.reverse();
576        }
577        return Ok(match spec {
578            "Int8" => Value::Float(b[0] as i8 as f64),
579            "Uint8" => Value::Float(b[0] as f64),
580            "Int16" => Value::Float(i16::from_le_bytes([b[0], b[1]]) as f64),
581            "Uint16" => Value::Float(u16::from_le_bytes([b[0], b[1]]) as f64),
582            "Int32" => Value::Float(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
583            "Uint32" => Value::Float(u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
584            "Float32" => Value::Float(f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
585            "Float64" => Value::Float(f64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]))),
586            "BigInt64" => {
587                let raw = i64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
588                with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
589            }
590            _ => {
591                let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
592                with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
593            }
594        });
595    }
596    let val = args.get(1).cloned().unwrap_or(Value::Undef);
597    let mut b = match spec {
598        "BigInt64" | "BigUint64" => {
599            use num_traits::cast::ToPrimitive;
600            // `setBigInt64`/`setBigUint64` take `ToBigInt(value)` (25.3.4.x via
601            // `SetViewValue` step 5), the same conversion an element write does.
602            let big = crate::builtins::to_bigint(&val)?;
603            let raw = if spec == "BigInt64" {
604                big.to_i64().unwrap_or(0) as u64
605            } else {
606                big.to_u64().unwrap_or(0)
607            };
608            raw.to_le_bytes().to_vec()
609        }
610        _ => {
611            let n = with_host(|h| h.to_number(&val));
612            match spec {
613                "Int8" | "Uint8" => vec![n as i64 as u8],
614                "Int16" | "Uint16" => (n as i64 as u16).to_le_bytes().to_vec(),
615                "Int32" | "Uint32" => (n as i64 as u32).to_le_bytes().to_vec(),
616                "Float32" => (n as f32).to_le_bytes().to_vec(),
617                _ => n.to_le_bytes().to_vec(),
618            }
619        }
620    };
621    if !le {
622        b.reverse();
623    }
624    write_view_bytes(recv, at, &b);
625    Ok(Value::Undef)
626}
627
628/// `ab.resize(n)` on a resizable buffer — grows with zeros or truncates,
629/// in place, so every view over it sees the new size.
630pub fn buffer_resize(ab: &Value, args: &[Value]) -> Result<Value, String> {
631    let max = with_host(|h| match h.get(ab) {
632        Some(JsObj::Object(p)) => p.get("@@maxByteLength").map(|m| h.to_number(m) as usize),
633        _ => None,
634    })
635    .ok_or_else(|| {
636        crate::host::type_error(
637            "ArrayBuffer.prototype.resize called on a non-resizable ArrayBuffer",
638        )
639    })?;
640    let n = super::arg_num(args, 0).max(0.0) as usize;
641    if n > max {
642        return Err(crate::host::range_error("Invalid array buffer length"));
643    }
644    let store = store_of(ab);
645    with_host(|h| {
646        if let Some(a) = store {
647            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
648                items.resize(n, Value::Float(0.0));
649            }
650        }
651        if let Some(JsObj::Object(p)) = h.get_mut(ab) {
652            p.insert("byteLength".into(), Value::Float(n as f64));
653        }
654    });
655    Ok(Value::Undef)
656}
657
658/// Overwrite an `ArrayBuffer`'s bytes wholesale, for a producer that computed
659/// them outside the heap.
660pub fn write_buffer_bytes(ab: &Value, bytes: &[u8]) {
661    let Some(store) = store_of(ab) else { return };
662    with_host(|h| {
663        if let Some(JsObj::Array(items)) = h.get_mut(&store) {
664            *items = bytes.iter().map(|b| Value::Float(*b as f64)).collect();
665        }
666        if let Some(JsObj::Object(p)) = h.get_mut(ab) {
667            p.insert("byteLength".into(), Value::Float(bytes.len() as f64));
668        }
669    });
670}
671
672/// The heap array an `ArrayBuffer` keeps its bytes in, so another view can
673/// share it rather than copy.
674pub fn buffer_store(ab: &Value) -> Option<Value> {
675    store_of(ab)
676}
677
678/// A COPY of an `ArrayBuffer`'s bytes, for the callers that only read.
679pub fn buffer_bytes_snapshot(ab: &Value) -> Option<Vec<u8>> {
680    let store = store_of(ab)?;
681    with_host(|h| match h.get(&store) {
682        Some(JsObj::Array(items)) => {
683            Some(items.iter().map(|x| h.to_number(x) as i64 as u8).collect())
684        }
685        _ => None,
686    })
687}
688
689/// An `ArrayBuffer`'s byte length, from its own store.
690pub fn buffer_byte_length(ab: &Value) -> usize {
691    with_host(|h| match h.get(ab) {
692        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
693            Some(JsObj::Array(items)) => items.len(),
694            _ => 0,
695        },
696        _ => 0,
697    })
698}
699
700/// `ArrayBuffer.prototype.slice(begin[, end])` — a COPY of the byte range, as a
701/// new buffer. Writes to it are not seen by views over the original.
702pub fn buffer_slice(ab: &Value, args: &[Value]) -> Value {
703    let total = buffer_byte_length(ab) as i64;
704    let idx = |v: Option<&Value>, dflt: i64| -> usize {
705        let n = match v {
706            None | Some(Value::Undef) => dflt,
707            Some(x) => with_host(|h| h.to_number(x)) as i64,
708        };
709        (if n < 0 { total + n } else { n }).clamp(0, total) as usize
710    };
711    let start = idx(args.first(), 0);
712    let end = idx(args.get(1), total).max(start);
713    let out = new_array_buffer(end - start);
714    let src = with_host(|h| match h.get(ab) {
715        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
716            Some(JsObj::Array(items)) => items[start..end].to_vec(),
717            _ => Vec::new(),
718        },
719        _ => Vec::new(),
720    });
721    with_host(|h| {
722        if let Some(JsObj::Object(p)) = h.get(&out) {
723            if let Some(arr) = p.get("@@bytes").cloned() {
724                if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
725                    *items = src;
726                }
727            }
728        }
729    });
730    out
731}
732
733/// Element vector for a typed-array construction from its first argument:
734/// a number → that many zeroed slots; an array/iterable/typed-array → its coerced
735/// values; otherwise → empty.
736fn build_elems(kind: &str, args: &[Value]) -> Result<Vec<Value>, String> {
737    match args.first() {
738        None | Some(Value::Undef) => Ok(Vec::new()),
739        // A non-Object first argument is a LENGTH (23.2.5.1 step 6), taken
740        // through ToIndex: `new Uint8Array('2')` is two zeros and a negative
741        // or unsafe length is a RangeError, not an empty array.
742        Some(v) if is_primitive(v) => {
743            let n_raw = super::arg_num(args, 0);
744            let n = to_index(n_raw).ok_or_else(|| {
745                crate::host::range_error(&format!(
746                    "Invalid typed array length: {}",
747                    fmt_number(n_raw)
748                ))
749            })?;
750            Ok(vec![zero_of(kind); n])
751        }
752        Some(v) => {
753            // Another typed array / Buffer → copy its elements; anything else
754            // iterable → coerce each entry.
755            let items = match super::native_tag(v).as_deref() {
756                Some("TypedArray") | Some("Buffer") => elem_values(v),
757                _ => crate::host::iter_all(v).unwrap_or_default(),
758            };
759            items.iter().map(|x| coerce_val(kind, x)).collect()
760        }
761    }
762}
763
764// ── Uint8Array base64/hex (the "Uint8Array to/from base64" proposal) ──────────
765
766/// How much of a trailing partial base64 chunk `fromBase64`/`setFromBase64`
767/// accept. The default is `loose`, which is why an UNPADDED string decodes.
768#[derive(Clone, Copy, PartialEq)]
769enum LastChunk {
770    Loose,
771    Strict,
772    StopBeforePartial,
773}
774
775/// Read the `{ alphabet, lastChunkHandling }` options object. Both reject an
776/// unknown value with node's `invalid option <v>`, and a non-object that is not
777/// `undefined` is `invalid_argument` — not the usual "must be an object".
778fn base64_options(opt: Option<&Value>) -> Result<(bool, LastChunk), String> {
779    let Some(o) = opt.filter(|v| !matches!(v, Value::Undef)) else {
780        return Ok((false, LastChunk::Loose));
781    };
782    if !with_host(|h| matches!(h.get(o), Some(JsObj::Object(_)))) {
783        return Err(crate::host::type_error("invalid_argument"));
784    }
785    let read = |k: &str| {
786        with_host(|h| match h.get(o) {
787            Some(JsObj::Object(p)) => p.get(k).filter(|v| !matches!(v, Value::Undef)).cloned(),
788            _ => None,
789        })
790    };
791    let url = match read("alphabet") {
792        None => false,
793        Some(v) => match with_host(|h| h.str_of(&v)).as_str() {
794            "base64" => false,
795            "base64url" => true,
796            other => return Err(crate::host::type_error(&format!("invalid option {other}"))),
797        },
798    };
799    let last = match read("lastChunkHandling") {
800        None => LastChunk::Loose,
801        Some(v) => match with_host(|h| h.str_of(&v)).as_str() {
802            "loose" => LastChunk::Loose,
803            "strict" => LastChunk::Strict,
804            "stop-before-partial" => LastChunk::StopBeforePartial,
805            other => return Err(crate::host::type_error(&format!("invalid option {other}"))),
806        },
807    };
808    Ok((url, last))
809}
810
811const B64_BAD: &str =
812    "SyntaxError: Found a character that cannot be part of a valid base64 string.";
813const B64_SINGLE: &str =
814    "SyntaxError: The base64 input terminates with a single character, excluding padding (=).";
815
816/// Decode base64 STRICTLY, reporting how many characters were consumed.
817///
818/// The lenient decoder behind `atob` cannot serve here: this has to reject a
819/// stray `=`, a wrong pad count and a character outside the selected alphabet,
820/// and `stop-before-partial` needs the consumed count rather than just the
821/// bytes. ASCII whitespace is skipped, which node also allows.
822fn decode_base64_strict(s: &str, url: bool, last: LastChunk) -> Result<(Vec<u8>, usize), String> {
823    let value = |c: char| -> Option<u32> {
824        let table = if url {
825            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
826        } else {
827            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
828        };
829        table.find(c).map(|i| i as u32)
830    };
831    let chars: Vec<char> = s.chars().collect();
832    let mut out = Vec::new();
833    let mut chunk: Vec<u32> = Vec::new();
834    let mut consumed = 0usize;
835    let mut i = 0usize;
836    while i < chars.len() {
837        let c = chars[i];
838        if c.is_ascii_whitespace() {
839            i += 1;
840            continue;
841        }
842        if c == '=' {
843            // Padding closes the chunk, and only a 2- or 3-character chunk may
844            // be padded: `QQ=` and `AA===` are both errors.
845            let pads = chars[i..].iter().filter(|c| **c == '=').count();
846            let rest_ok = chars[i..]
847                .iter()
848                .all(|c| *c == '=' || c.is_ascii_whitespace());
849            let want = 4 - chunk.len();
850            if !rest_ok || chunk.len() < 2 || pads != want {
851                return Err(B64_BAD.into());
852            }
853            out.extend(flush_base64_chunk(&chunk));
854            return Ok((out, chars.len()));
855        }
856        let Some(v) = value(c) else {
857            return Err(B64_BAD.into());
858        };
859        chunk.push(v);
860        i += 1;
861        if chunk.len() == 4 {
862            out.extend(flush_base64_chunk(&chunk));
863            chunk.clear();
864            consumed = i;
865        }
866    }
867    match chunk.len() {
868        0 => Ok((out, consumed)),
869        // A single leftover character encodes nothing at all.
870        1 if last != LastChunk::StopBeforePartial => Err(B64_SINGLE.into()),
871        _ if last == LastChunk::StopBeforePartial => Ok((out, consumed)),
872        1 => Ok((out, consumed)),
873        _ if last == LastChunk::Strict => Err(B64_SINGLE.into()),
874        _ => {
875            out.extend(flush_base64_chunk(&chunk));
876            Ok((out, chars.len()))
877        }
878    }
879}
880
881/// The 1-3 bytes a base64 chunk of 2, 3 or 4 sextets encodes.
882fn flush_base64_chunk(chunk: &[u32]) -> Vec<u8> {
883    let mut acc = 0u32;
884    for v in chunk {
885        acc = (acc << 6) | v;
886    }
887    let bytes = chunk.len() - 1;
888    acc <<= 6 * (4 - chunk.len());
889    let all = [(acc >> 16) as u8, (acc >> 8) as u8, acc as u8];
890    all[..bytes].to_vec()
891}
892
893const HEX_BAD: &str = "SyntaxError: Input string must contain hex characters in even length";
894
895/// Decode hex STRICTLY. Node reports the same message for an odd length and for
896/// a non-hex character, so `"gg"` and `"0"` fail identically.
897fn decode_hex_strict(s: &str) -> Result<Vec<u8>, String> {
898    let chars: Vec<char> = s.chars().collect();
899    if chars.len() % 2 != 0 || !chars.iter().all(|c| c.is_ascii_hexdigit()) {
900        return Err(HEX_BAD.into());
901    }
902    Ok(chars
903        .chunks(2)
904        .map(|p| {
905            let hi = p[0].to_digit(16).expect("checked");
906            let lo = p[1].to_digit(16).expect("checked");
907            (hi * 16 + lo) as u8
908        })
909        .collect())
910}
911
912/// The string argument these six all take, rejecting anything else the way node
913/// does rather than coercing it.
914fn base64_input(args: &[Value]) -> Result<String, String> {
915    let v = args.first().cloned().unwrap_or(Value::Undef);
916    let is_str = matches!(v, Value::Str(_))
917        || with_host(|h| matches!(h.get(&v), Some(crate::host::JsObj::Str(_))));
918    if !is_str {
919        return Err(crate::host::type_error("input argument must be a string"));
920    }
921    Ok(with_host(|h| h.str_of(&v)))
922}
923
924/// `Uint8Array.fromBase64` / `Uint8Array.fromHex`.
925fn from_base64_static(method: &str, args: &[Value]) -> Result<Value, String> {
926    let s = base64_input(args)?;
927    let bytes = if method == "fromHex" {
928        decode_hex_strict(&s)?
929    } else {
930        let (url, last) = base64_options(args.get(1))?;
931        decode_base64_strict(&s, url, last)?.0
932    };
933    Ok(make(
934        "Uint8Array",
935        bytes.iter().map(|b| Value::Float(*b as f64)).collect(),
936    ))
937}
938
939/// `Uint8Array.from(iterable[, mapFn])` / `Uint8Array.of(...items)`, and the
940/// base64/hex statics.
941pub fn static_call(kind: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
942    // The base64/hex statics are on `Uint8Array` ONLY — no other view has them.
943    if matches!(method, "fromBase64" | "fromHex") {
944        if kind != "Uint8Array" {
945            return None;
946        }
947        return Some(from_base64_static(method, args));
948    }
949    Some(match method {
950        "of" => args
951            .iter()
952            .map(|x| coerce_val(kind, x))
953            .collect::<Result<Vec<Value>, String>>()
954            .map(|e| make(kind, e)),
955        "from" => from(kind, args),
956        // `ArrayBuffer.isView(x)` — true for a typed array or a Buffer (which is
957        // a Uint8Array view), false for the backing ArrayBuffer itself.
958        "isView" => Ok(Value::Bool(with_host(|h| {
959            matches!(
960                h.get(&args.first().cloned().unwrap_or(Value::Undef)),
961                Some(crate::host::JsObj::Object(p))
962                    if matches!(
963                        p.get("@@native").map(|t| h.str_of(t)).as_deref(),
964                        Some("TypedArray") | Some("Buffer") | Some("DataView")
965                    )
966            )
967        }))),
968        _ => return None,
969    })
970}
971
972fn from(kind: &str, args: &[Value]) -> Result<Value, String> {
973    let src = args.first().cloned().unwrap_or(Value::Undef);
974    let map_fn = args
975        .get(1)
976        .cloned()
977        .filter(|f| with_host(|h| crate::host::is_callable(h, f)));
978    // A source that is not iterable is read as an array-like (23.2.2.1
979    // step 7): `Int8Array.from({length: 3, 1: 5})` is `[0, 5, 0]`.
980    let items = if let Some(e) = elems_of(&src) {
981        e.into_iter().map(Value::Float).collect()
982    } else {
983        crate::host::iter_all(&src)
984            .unwrap_or_else(|_| crate::builtins::array_like_items(&src))
985    };
986    let mut out = Vec::with_capacity(items.len());
987    for (i, it) in items.into_iter().enumerate() {
988        let mapped = match &map_fn {
989            Some(f) => crate::host::invoke(f, vec![it, Value::Float(i as f64)], None)?,
990            None => it,
991        };
992        out.push(coerce_val(kind, &mapped)?);
993    }
994    Ok(make(kind, out))
995}
996
997/// The element values of a typed array / Buffer (`None` for anything else).
998pub fn elems_of(v: &Value) -> Option<Vec<f64>> {
999    let tag = super::native_tag(v)?;
1000    if !matches!(tag.as_str(), "TypedArray" | "Buffer") {
1001        return None;
1002    }
1003    let vals = elem_values(v);
1004    Some(with_host(|h| vals.iter().map(|x| h.to_number(x)).collect()))
1005}
1006
1007/// The number of elements `v` exposes as integer-index own properties, for a
1008/// typed array (its view length) or a `Buffer` (`@@bytes`); `None` otherwise.
1009///
1010/// Both index-membership questions — `obj.hasOwnProperty(i)` and `i in obj` —
1011/// must answer from this one place. They used to disagree: `hasOwnProperty`
1012/// carried a hand-rolled arm that understood `@@bytes` only, so it was right for
1013/// a Buffer and wrong for every other typed array, while the `in` operator knew
1014/// about neither and reported false for every valid index of both.
1015pub fn index_len(v: &Value) -> Option<usize> {
1016    match super::native_tag(v)?.as_str() {
1017        "TypedArray" => Some(view_len(v)),
1018        "Buffer" => with_host(|h| match h.get(v) {
1019            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
1020                Some(JsObj::Array(items)) => Some(items.len()),
1021                _ => None,
1022            },
1023            _ => None,
1024        }),
1025        _ => None,
1026    }
1027}
1028
1029/// Whether `key` is an in-range integer index of the typed array / Buffer `v`.
1030/// `None` when `v` is neither, so callers can fall through to their own logic.
1031pub fn has_index(v: &Value, key: &str) -> Option<bool> {
1032    let len = index_len(v)?;
1033    Some(key.parse::<usize>().map(|i| i < len).unwrap_or(false))
1034}
1035
1036/// The `@@kind` of a typed-array receiver (defaults to `Uint8Array`).
1037pub fn kind_of(recv: &Value) -> String {
1038    with_host(|h| match h.get(recv) {
1039        Some(JsObj::Object(p)) => p
1040            .get("@@kind")
1041            .map(|v| h.str_of(v))
1042            .unwrap_or_else(|| "Uint8Array".into()),
1043        _ => "Uint8Array".into(),
1044    })
1045}
1046
1047// ── backing store ────────────────────────────────────────────────────────────
1048//
1049// Every view — typed array or `DataView` — reads and writes THROUGH an
1050// `ArrayBuffer`, which owns the only copy of the bytes as a hidden `@@bytes`
1051// heap array. That is what makes two views over one buffer see each other's
1052// writes: `new Uint32Array(ab)[0]` reflects a byte written through
1053// `new Uint8Array(ab)`. Before this an `ArrayBuffer` carried nothing but a
1054// `byteLength` and each view owned a private element vector, so nothing was
1055// ever shared and `DataView` did not exist at all.
1056
1057/// Allocate an `ArrayBuffer` of `n` zeroed bytes.
1058pub fn new_array_buffer(n: usize) -> Value {
1059    with_host(|h| {
1060        let arr = h.new_array(vec![Value::Float(0.0); n]);
1061        let mut m = IndexMap::new();
1062        m.insert("@@native".into(), h.new_str("ArrayBuffer"));
1063        m.insert("@@bytes".into(), arr);
1064        m.insert("byteLength".into(), Value::Float(n as f64));
1065        // `detached` is a prototype accessor in the spec; kept as a hidden own
1066        // property here so it reads back without appearing in `Object.keys` or
1067        // `console.log`, the same way `byteLength` is.
1068        m.insert("detached".into(), Value::Bool(false));
1069        // A FIXED buffer still reports both, as `false` and its own length —
1070        // they are prototype accessors in the spec, so they always answer.
1071        m.insert("resizable".into(), Value::Bool(false));
1072        m.insert("maxByteLength".into(), Value::Float(n as f64));
1073        let obj = h.new_object(m);
1074        for k in ["byteLength", "detached", "resizable", "maxByteLength"] {
1075            h.hide_prop(&obj, k);
1076        }
1077        // `ensure_ctor_proto` builds the prototype WITH a `constructor` slot, so
1078        // `ab.constructor.name` reports `ArrayBuffer` rather than `Object`.
1079        if let Some(p) = h.ensure_ctor_proto("ArrayBuffer") {
1080            h.set_proto(&obj, p);
1081        }
1082        obj
1083    })
1084}
1085
1086/// Whether `ab` has been DETACHED — its bytes handed to another buffer by
1087/// `transfer`, or given away by `structuredClone`'s `transfer` option.
1088///
1089/// A detached buffer is not an empty one: reading a view over it answers
1090/// `undefined` and its `length` is 0, but every METHOD on that view throws.
1091pub fn is_detached(ab: &Value) -> bool {
1092    with_host(|h| match h.get(ab) {
1093        Some(JsObj::Object(p)) => p.get("detached").map(|v| h.truthy(v)).unwrap_or(false),
1094        _ => false,
1095    })
1096}
1097
1098/// Whether `v` is a view whose backing buffer has been detached.
1099pub fn view_detached(v: &Value) -> bool {
1100    with_host(|h| view_detached_h(h, v))
1101}
1102
1103/// `view_detached` for a caller that already holds the host borrow — the
1104/// iteration entry point runs under one, and re-entering aborts the process.
1105pub fn view_detached_h(h: &crate::host::JsHost, v: &Value) -> bool {
1106    let buf = match h.get(v) {
1107        Some(JsObj::Object(p)) => p.get("@@buffer").cloned(),
1108        _ => None,
1109    };
1110    match buf.and_then(|b| match h.get(&b) {
1111        Some(JsObj::Object(p)) => p.get("detached").cloned(),
1112        _ => None,
1113    }) {
1114        Some(d) => h.truthy(&d),
1115        None => false,
1116    }
1117}
1118
1119/// Detach `ab`: drop its bytes and mark it, so every later read reports zero
1120/// length and every method over it throws.
1121pub fn detach_buffer(ab: &Value) {
1122    detach(ab)
1123}
1124
1125fn detach(ab: &Value) {
1126    with_host(|h| {
1127        let empty = h.new_array(Vec::new());
1128        if let Some(JsObj::Object(p)) = h.get_mut(ab) {
1129            p.insert("@@bytes".into(), empty);
1130            p.insert("byteLength".into(), Value::Float(0.0));
1131            p.insert("detached".into(), Value::Bool(true));
1132        }
1133        h.hide_prop(ab, "byteLength");
1134        h.hide_prop(ab, "detached");
1135    });
1136}
1137
1138/// `ArrayBuffer.prototype.transfer([newLength])` and `transferToFixedLength`.
1139///
1140/// A fresh buffer takes the bytes — truncated or zero-padded to `newLength` —
1141/// and the receiver is detached. The two differ only in whether the result may
1142/// still grow.
1143pub fn buffer_transfer(ab: &Value, args: &[Value], fixed: bool) -> Result<Value, String> {
1144    let method = if fixed {
1145        "transferToFixedLength"
1146    } else {
1147        "transfer"
1148    };
1149    if is_detached(ab) {
1150        return Err(crate::host::type_error(&format!(
1151            "Cannot perform ArrayBuffer.prototype.{method} on a detached ArrayBuffer"
1152        )));
1153    }
1154    let old = byte_len_of(ab);
1155    let new_len = match args.first().filter(|v| !matches!(v, Value::Undef)) {
1156        Some(v) => with_host(|h| h.to_number(v)).max(0.0) as usize,
1157        None => old,
1158    };
1159    let mut bytes = view_bytes_of_buffer(ab, old);
1160    bytes.resize(new_len, 0);
1161    let out = new_array_buffer(new_len);
1162    write_buffer_bytes(&out, &bytes);
1163    if !fixed {
1164        // `transfer` keeps the source's resizability; `transferToFixedLength`
1165        // never does.
1166        let resizable = with_host(|h| match h.get(ab) {
1167            Some(JsObj::Object(p)) => p.contains_key("@@maxByteLength"),
1168            _ => false,
1169        });
1170        if resizable {
1171            let max = with_host(|h| match h.get(ab) {
1172                Some(JsObj::Object(p)) => p.get("@@maxByteLength").cloned(),
1173                _ => None,
1174            });
1175            if let Some(max) = max {
1176                with_host(|h| {
1177                    if let Some(JsObj::Object(p)) = h.get_mut(&out) {
1178                        p.insert("@@maxByteLength".into(), max);
1179                    }
1180                });
1181            }
1182        }
1183    }
1184    detach(ab);
1185    Ok(out)
1186}
1187
1188/// An ArrayBuffer's `byteLength`.
1189fn byte_len_of(ab: &Value) -> usize {
1190    with_host(|h| match h.get(ab) {
1191        Some(JsObj::Object(p)) => {
1192            p.get("byteLength").map(|l| h.to_number(l)).unwrap_or(0.0) as usize
1193        }
1194        _ => 0,
1195    })
1196}
1197
1198/// An ArrayBuffer's bytes.
1199fn view_bytes_of_buffer(ab: &Value, n: usize) -> Vec<u8> {
1200    let Some(store) = store_of(ab) else {
1201        return Vec::new();
1202    };
1203    with_host(|h| match h.get(&store) {
1204        Some(JsObj::Array(items)) => items
1205            .iter()
1206            .take(n)
1207            .map(|x| h.to_number(x) as i64 as u8)
1208            .collect(),
1209        _ => Vec::new(),
1210    })
1211}
1212
1213/// The TypeError a method over a DETACHED buffer throws. Node names the method
1214/// and distinguishes a view's from a DataView's from the buffer's own.
1215pub fn detached_error(label: &str, method: &str, buffer_only: bool) -> String {
1216    let tail = if buffer_only {
1217        "a detached ArrayBuffer"
1218    } else {
1219        "a detached or out-of-bounds ArrayBuffer"
1220    };
1221    // A symbol-keyed member reports the name of the function it ALIASES, the way
1222    // node does everywhere else (`Set.prototype.keys` reports `values`):
1223    // `[...detachedView]` says `%TypedArray%.prototype.values`, never
1224    // `.@@iterator`, which is this frontend's internal spelling for
1225    // `Symbol.iterator` and not a name any script wrote.
1226    let method = match method {
1227        "@@iterator" => "values",
1228        other => other,
1229    };
1230    crate::host::type_error(&format!("Cannot perform {label}.{method} on {tail}"))
1231}
1232
1233/// The heap array holding an `ArrayBuffer`'s bytes.
1234fn store_of(ab: &Value) -> Option<Value> {
1235    with_host(|h| match h.get(ab) {
1236        Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
1237        _ => None,
1238    })
1239}
1240
1241/// A view's `(buffer, byteOffset)`.
1242fn view_base(v: &Value) -> Option<(Value, usize)> {
1243    with_host(|h| match h.get(v) {
1244        Some(JsObj::Object(p)) => {
1245            let buf = p.get("@@buffer").cloned()?;
1246            let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0);
1247            Some((buf, off.max(0.0) as usize))
1248        }
1249        _ => None,
1250    })
1251}
1252
1253/// `n` bytes of `v`'s buffer starting at its `byteOffset + at`.
1254pub fn view_bytes(v: &Value, at: usize, n: usize) -> Option<Vec<u8>> {
1255    let (buf, off) = view_base(v)?;
1256    let store = store_of(&buf)?;
1257    with_host(|h| match h.get(&store) {
1258        Some(JsObj::Array(items)) => {
1259            let start = off + at;
1260            if start + n > items.len() {
1261                return None;
1262            }
1263            Some(
1264                items[start..start + n]
1265                    .iter()
1266                    .map(|x| h.to_number(x) as i64 as u8)
1267                    .collect(),
1268            )
1269        }
1270        _ => None,
1271    })
1272}
1273
1274/// Write `bytes` into `v`'s buffer at its `byteOffset + at`. False when the
1275/// range does not fit.
1276pub fn write_view_bytes(v: &Value, at: usize, bytes: &[u8]) -> bool {
1277    let Some((buf, off)) = view_base(v) else {
1278        return false;
1279    };
1280    let Some(store) = store_of(&buf) else {
1281        return false;
1282    };
1283    with_host(|h| match h.get_mut(&store) {
1284        Some(JsObj::Array(items)) => {
1285            let start = off + at;
1286            if start + bytes.len() > items.len() {
1287                return false;
1288            }
1289            for (i, b) in bytes.iter().enumerate() {
1290                items[start + i] = Value::Float(*b as f64);
1291            }
1292            true
1293        }
1294        _ => false,
1295    })
1296}
1297
1298/// Decode one element of `kind` from its `bytes` (native byte order, which on
1299/// every architecture this runs on is little-endian).
1300fn decode(kind: &str, b: &[u8]) -> Value {
1301    match kind {
1302        "Int8Array" => Value::Float(b[0] as i8 as f64),
1303        "Uint8Array" | "Uint8ClampedArray" => Value::Float(b[0] as f64),
1304        "Int16Array" => Value::Float(i16::from_le_bytes([b[0], b[1]]) as f64),
1305        "Uint16Array" => Value::Float(u16::from_le_bytes([b[0], b[1]]) as f64),
1306        "Int32Array" => Value::Float(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
1307        "Uint32Array" => Value::Float(u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
1308        "Float32Array" => Value::Float(f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
1309        "BigInt64Array" => {
1310            let raw = i64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1311            with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
1312        }
1313        "BigUint64Array" => {
1314            let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1315            with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
1316        }
1317        _ => Value::Float(f64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]))),
1318    }
1319}
1320
1321/// Encode one already-coerced element of `kind` into its bytes.
1322fn encode(kind: &str, v: &Value) -> Vec<u8> {
1323    if is_bigint_kind(kind) {
1324        use num_traits::cast::ToPrimitive;
1325        let b = bigint_of(v);
1326        let raw = if kind == "BigInt64Array" {
1327            b.to_i64().unwrap_or(0) as u64
1328        } else {
1329            b.to_u64().unwrap_or(0)
1330        };
1331        return raw.to_le_bytes().to_vec();
1332    }
1333    let n = num(v);
1334    match kind {
1335        "Int8Array" => vec![n as i64 as i8 as u8],
1336        "Uint8Array" | "Uint8ClampedArray" => vec![n as i64 as u8],
1337        "Int16Array" => (n as i64 as i16).to_le_bytes().to_vec(),
1338        "Uint16Array" => (n as i64 as u16).to_le_bytes().to_vec(),
1339        "Int32Array" => (n as i64 as i32).to_le_bytes().to_vec(),
1340        "Uint32Array" => (n as i64 as u32).to_le_bytes().to_vec(),
1341        "Float32Array" => (n as f32).to_le_bytes().to_vec(),
1342        _ => n.to_le_bytes().to_vec(),
1343    }
1344}
1345
1346/// The elements of a typed-array view, decoded with a host borrow ALREADY
1347/// held. `host` reads views from inside `&self` methods (inspect, key
1348/// enumeration, iteration) where re-entering through `with_host` would panic on
1349/// the outstanding borrow.
1350pub fn elems_with_host(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
1351    if let Some(JsObj::Object(p)) = h.get(v) {
1352        if let Some(arr) = p.get("@@bytes") {
1353            return match h.get(arr) {
1354                Some(JsObj::Array(items)) => items.clone(),
1355                _ => Vec::new(),
1356            };
1357        }
1358    }
1359    let Some((kind, raws)) = raw_elems(h, v) else {
1360        return Vec::new();
1361    };
1362    // A 64-bit element is a BigInt, which needs an allocation this borrow
1363    // cannot make; `elems_mut_host` is the reader for callers that can.
1364    if is_bigint_kind(&kind) {
1365        return vec![Value::Undef; raws.len()];
1366    }
1367    raws.iter().map(|b| decode(&kind, b)).collect()
1368}
1369
1370/// The raw bytes of every element of a view, with the host borrow already held.
1371/// The shared half of the three readers below.
1372fn raw_elems(h: &crate::host::JsHost, v: &Value) -> Option<(String, Vec<Vec<u8>>)> {
1373    let JsObj::Object(p) = h.get(v)? else {
1374        return None;
1375    };
1376    let kind = p
1377        .get("@@kind")
1378        .map(|k| h.str_of(k))
1379        .unwrap_or_else(|| "Uint8Array".into());
1380    let bpe = bytes_per_element(&kind);
1381    // A view over a DETACHED buffer has no elements. Its own `length` still
1382    // holds the old count, so `util.inspect` showed `Uint8Array(4) [0,0,0,0]`
1383    // over a buffer with no bytes left.
1384    let len = if view_detached_h(h, v) {
1385        0
1386    } else {
1387        p.get("length").map(|l| h.to_number(l)).unwrap_or(0.0) as usize
1388    };
1389    let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0) as usize;
1390    let store = match p.get("@@buffer").and_then(|b| h.get(b)) {
1391        Some(JsObj::Object(bp)) => bp.get("@@bytes").and_then(|a| h.get(a)),
1392        _ => None,
1393    };
1394    let JsObj::Array(bytes) = store? else {
1395        return None;
1396    };
1397    let out = (0..len)
1398        .map(|i| {
1399            let start = off + i * bpe;
1400            if start + bpe > bytes.len() {
1401                return vec![0u8; bpe];
1402            }
1403            bytes[start..start + bpe]
1404                .iter()
1405                .map(|x| h.to_number(x) as i64 as u8)
1406                .collect()
1407        })
1408        .collect();
1409    Some((kind, out))
1410}
1411
1412/// The elements of a view with a MUTABLE host borrow held, so the two 64-bit
1413/// kinds can allocate their BigInts. This is the complete reader; the `&self`
1414/// one below cannot allocate and so answers `undefined` for those two kinds.
1415pub fn elems_mut_host(h: &mut crate::host::JsHost, v: &Value) -> Vec<Value> {
1416    if let Some(JsObj::Object(p)) = h.get(v) {
1417        if let Some(arr) = p.get("@@bytes").cloned() {
1418            return match h.get(&arr) {
1419                Some(JsObj::Array(items)) => items.clone(),
1420                _ => Vec::new(),
1421            };
1422        }
1423    }
1424    let Some((kind, raws)) = raw_elems(h, v) else {
1425        return Vec::new();
1426    };
1427    raws.iter()
1428        .map(|b| {
1429            if !is_bigint_kind(&kind) {
1430                return decode(&kind, b);
1431            }
1432            let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1433            h.new_bigint(if kind == "BigInt64Array" {
1434                num_bigint::BigInt::from(raw as i64)
1435            } else {
1436                num_bigint::BigInt::from(raw)
1437            })
1438        })
1439        .collect()
1440}
1441
1442/// Every element rendered for display, for `util.inspect` — which holds a
1443/// shared borrow and so cannot allocate the BigInt a 64-bit element would need
1444/// as a `Value`. Elements are always primitives, so a string loses nothing.
1445pub fn elems_display(h: &crate::host::JsHost, v: &Value) -> Vec<String> {
1446    let Some((kind, raws)) = raw_elems(h, v) else {
1447        return Vec::new();
1448    };
1449    raws.iter()
1450        .map(|b| {
1451            if !is_bigint_kind(&kind) {
1452                return h.inspect(&decode(&kind, b));
1453            }
1454            let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1455            if kind == "BigInt64Array" {
1456                format!("{}n", raw as i64)
1457            } else {
1458                format!("{raw}n")
1459            }
1460        })
1461        .collect()
1462}
1463
1464/// The element count a view exposes, from its own `length` slot.
1465fn view_len(v: &Value) -> usize {
1466    // A view over a DETACHED buffer has length 0 — its own `length` property
1467    // still holds the old count, which is why this cannot just read it.
1468    if view_detached(v) {
1469        return 0;
1470    }
1471    with_host(|h| match h.get(v) {
1472        Some(JsObj::Object(p)) => p.get("length").map(|l| h.to_number(l)).unwrap_or(0.0) as usize,
1473        _ => 0,
1474    })
1475}
1476
1477// ── element indexing (called from builtins::get_property/set_property) ────────
1478
1479/// `ta[i]` read: the element at char/index `i`, or `None` if `i` is out of range
1480/// or not an integer index.
1481pub fn elem_get(recv: &Value, key: &str) -> Option<Value> {
1482    let i: usize = key.parse().ok()?;
1483    if i >= view_len(recv) {
1484        return None;
1485    }
1486    let kind = kind_of(recv);
1487    let bpe = bytes_per_element(&kind);
1488    let bytes = view_bytes(recv, i * bpe, bpe)?;
1489    Some(decode(&kind, &bytes))
1490}
1491
1492/// `ta[i] = v` write (coerced to the kind). Returns true if `i` is a valid index.
1493pub fn elem_set(recv: &Value, key: &str, val: &Value) -> Result<bool, String> {
1494    let Ok(i) = key.parse::<usize>() else {
1495        return Ok(false);
1496    };
1497    let kind = kind_of(recv);
1498    // Coerced through the element type, so writing a Number into a 64-bit view
1499    // throws rather than storing an un-typed element.
1500    let n = coerce_val(&kind, val)?;
1501    if i >= view_len(recv) {
1502        return Ok(false);
1503    }
1504    let bpe = bytes_per_element(&kind);
1505    Ok(write_view_bytes(recv, i * bpe, &encode(&kind, &n)))
1506}
1507
1508/// Build a result of the same "species" as `recv`: a `Buffer` receiver yields a
1509/// `Buffer`, every other typed array yields its own kind. Node picks the result
1510/// type from the receiver's constructor, so `Buffer.from([1]).map(f)` is a
1511/// Buffer and `new Int32Array([1]).map(f)` is an `Int32Array`.
1512fn species(recv: &Value, kind: &str, elems: Vec<Value>) -> Value {
1513    if super::native_tag(recv).as_deref() == Some("Buffer") {
1514        let bytes: Vec<u8> = elems.iter().map(|x| num(x) as i64 as u8).collect();
1515        return super::buffer::from_bytes(&bytes);
1516    }
1517    make(kind, elems)
1518}
1519
1520/// Overwrite `recv`'s elements in place, for the methods that mutate and return
1521/// the receiver (`fill`, `reverse`, `sort`, `copyWithin`). Writes through to
1522/// whichever store backs it — the `ArrayBuffer` for a typed array, `@@bytes` for
1523/// a `Buffer`.
1524fn write_elems(recv: &Value, kind: &str, vals: &[Value]) -> Result<(), String> {
1525    if super::native_tag(recv).as_deref() == Some("TypedArray") {
1526        let bpe = bytes_per_element(kind);
1527        let coerced: Vec<Value> = vals
1528            .iter()
1529            .map(|v| coerce_val(kind, v))
1530            .collect::<Result<_, _>>()?;
1531        for (i, v) in coerced.iter().enumerate() {
1532            write_view_bytes(recv, i * bpe, &encode(kind, v));
1533        }
1534        return Ok(());
1535    }
1536    let field = "@@bytes";
1537    // Coerce OUTSIDE the host borrow: `coerce_val` re-enters the host to read a
1538    // BigInt and to allocate the wrapped one.
1539    let coerced: Vec<Value> = vals
1540        .iter()
1541        .map(|v| coerce_val(kind, v))
1542        .collect::<Result<_, _>>()?;
1543    with_host(|h| {
1544        if let Some(JsObj::Object(p)) = h.get(recv) {
1545            if let Some(arr) = p.get(field).cloned() {
1546                if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
1547                    for (i, v) in coerced.into_iter().enumerate() {
1548                        if i < items.len() {
1549                            items[i] = v;
1550                        }
1551                    }
1552                }
1553            }
1554        }
1555    });
1556    Ok(())
1557}
1558
1559/// Order `elems` the way `%TypedArray%.prototype.sort` (23.2.3.29) does, with
1560/// `cmp` as the optional user comparator. Shared with `toSorted` (23.2.3.33),
1561/// which is the same ordering over a copy.
1562fn sort_elements(elems: &mut Vec<Value>, kind: &str, cmp: Option<&Value>) -> Result<(), String> {
1563    let cmp = cmp.cloned().unwrap_or(Value::Undef);
1564    if with_host(|h| crate::host::is_callable(h, &cmp)) {
1565        // A user comparator goes through the same fallible merge sort
1566        // `Array.prototype.sort` uses: O(n log n) rather than the insertion sort
1567        // this was, and a comparator returning NaN keeps the pair's order
1568        // (23.2.4.1 step 3: NaN is +0) instead of swapping, which the `<= 0.0`
1569        // break got wrong.
1570        return crate::builtins::sort_values(elems, Some(&cmp));
1571    }
1572    // A typed array sorts NUMERICALLY by default, unlike `Array` which sorts by
1573    // string. Verified against node v26.7.0: `new Uint8Array([10,9,1]).sort()`
1574    // is `1,9,10` while `[10,9,1].sort()` is `1,10,9`.
1575    // A BigInt element cannot be ordered through an `f64` without collapsing
1576    // values more than 2^53 apart, so the 64-bit views compare the integers
1577    // themselves.
1578    if is_bigint_kind(kind) {
1579        let keys: Vec<num_bigint::BigInt> = elems.iter().map(bigint_of).collect();
1580        let mut idx: Vec<usize> = (0..elems.len()).collect();
1581        idx.sort_by(|a, b| keys[*a].cmp(&keys[*b]));
1582        *elems = idx.into_iter().map(|i| elems[i].clone()).collect();
1583    } else {
1584        elems.sort_by(|a, b| {
1585            num(a)
1586                .partial_cmp(&num(b))
1587                .unwrap_or(std::cmp::Ordering::Equal)
1588        });
1589    }
1590    Ok(())
1591}
1592
1593/// Resolve a relative index argument against `len` (negative counts from the
1594/// end), clamped into range — the `RelativeIndex` coercion the typed-array
1595/// methods share.
1596fn rel_index(args: &[Value], idx: usize, len: usize, default: usize) -> usize {
1597    if args.len() <= idx {
1598        return default;
1599    }
1600    let n = super::arg_num(args, idx);
1601    if n < 0.0 {
1602        (len as f64 + n).max(0.0) as usize
1603    } else {
1604        (n as usize).min(len)
1605    }
1606}
1607
1608/// Typed-array instance methods.
1609/// `toBase64` / `toHex` / `setFromBase64` / `setFromHex` — the `Uint8Array`
1610/// half of the base64/hex proposal. All four are brand-checked to `Uint8Array`:
1611/// every other view, and an ordinary array, is an incompatible receiver.
1612fn base64_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
1613    let kind = kind_of(recv);
1614    if kind != "Uint8Array" {
1615        // Node renders a non-view receiver by its brand and a WRONG view as
1616        // `undefined`, which reads oddly but is what it prints.
1617        // A typed array of the WRONG element kind renders as `undefined` here,
1618        // which reads oddly but is what node prints; every other receiver is
1619        // rendered the way the other brand checks render one, and never reaches
1620        // this arm — the dispatcher's guard catches it first.
1621        return Err(crate::host::type_error(&format!(
1622            "Method Uint8Array.prototype.{method} called on incompatible receiver undefined"
1623        )));
1624    }
1625    let bytes: Vec<u8> = elem_values(recv)
1626        .iter()
1627        .map(|v| with_host(|h| h.to_number(v)) as u8)
1628        .collect();
1629    match method {
1630        "toBase64" => {
1631            let (url, _) = base64_options(args.first())?;
1632            let omit = args
1633                .first()
1634                .filter(|v| !matches!(v, Value::Undef))
1635                .map(|o| {
1636                    with_host(|h| match h.get(o) {
1637                        Some(JsObj::Object(p)) => {
1638                            p.get("omitPadding").map(|v| h.truthy(v)).unwrap_or(false)
1639                        }
1640                        _ => false,
1641                    })
1642                })
1643                .unwrap_or(false);
1644            // The url alphabet only swaps the two characters — it does NOT drop
1645            // the padding, which `to_base64url` does for the `atob` callers.
1646            let mut s = super::to_base64(&bytes);
1647            if url {
1648                s = s.replace('+', "-").replace('/', "_");
1649            }
1650            if omit {
1651                s = s.trim_end_matches('=').to_string();
1652            }
1653            Ok(with_host(|h| h.new_str(s)))
1654        }
1655        "toHex" => Ok(with_host(|h| h.new_str(super::to_hex(&bytes)))),
1656        // `setFrom*` writes as much as FITS and reports how far it got, so a
1657        // short target is not an error — it stops at the last whole chunk.
1658        "setFromBase64" | "setFromHex" => {
1659            let s = base64_input(args)?;
1660            let (decoded, read) = if method == "setFromHex" {
1661                let d = decode_hex_strict(&s)?;
1662                let fits = d.len().min(bytes.len());
1663                (d[..fits].to_vec(), fits * 2)
1664            } else {
1665                let (url, last) = base64_options(args.get(1))?;
1666                // Decode only as much as the target can hold: whole 4-character
1667                // chunks, plus the final partial one when it still fits.
1668                let whole = (bytes.len() / 3) * 4;
1669                let head: String = s.chars().take(whole).collect();
1670                let (mut d, mut consumed) = decode_base64_strict(&head, url, last)?;
1671                if d.len() < bytes.len() {
1672                    let (full, full_read) = decode_base64_strict(&s, url, last)?;
1673                    if full.len() <= bytes.len() {
1674                        d = full;
1675                        consumed = full_read;
1676                    }
1677                }
1678                (d, consumed)
1679            };
1680            write_view_bytes(recv, 0, &decoded);
1681            Ok(with_host(|h| {
1682                let mut m = IndexMap::new();
1683                m.insert("read".to_string(), Value::Float(read as f64));
1684                m.insert("written".to_string(), Value::Float(decoded.len() as f64));
1685                h.new_object(m)
1686            }))
1687        }
1688        _ => unreachable!("caller gates the method name"),
1689    }
1690}
1691
1692pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
1693    // Every method over a DETACHED buffer throws, naming itself. An element
1694    // read and `length` answer zero instead, which is why this is a per-method
1695    // guard rather than a check inside the element accessors.
1696    if view_detached(recv) {
1697        return Err(detached_error("%TypedArray%.prototype", method, false));
1698    }
1699    if matches!(
1700        method,
1701        "toBase64" | "toHex" | "setFromBase64" | "setFromHex"
1702    ) {
1703        return base64_instance_call(recv, method, args);
1704    }
1705    let kind = kind_of(recv);
1706    // Elements travel as `Value`, not `f64`: a 64-bit view's are BigInts, and
1707    // rounding them through a double is exactly the loss those views exist to
1708    // avoid. The numeric kinds still hold `Value::Float`, so nothing about them
1709    // changes.
1710    let elems = elem_values(recv);
1711    // The callback-taking methods share one shape: invoke `cb(value, index,
1712    // receiver)` per element. They are inherited by `Buffer` too, which is why
1713    // they must live here rather than in either concrete type.
1714    // `forEach(fn, thisArg)` and its siblings bind `thisArg` as the callback's
1715    // `this`; it was being dropped, so `this` inside the callback was undefined.
1716    let this_arg = args.get(1).filter(|v| !matches!(v, Value::Undef)).cloned();
1717    let call_cb = |i: usize, v: &Value| -> Result<Value, String> {
1718        crate::host::invoke(
1719            &args.first().cloned().unwrap_or(Value::Undef),
1720            vec![v.clone(), Value::Float(i as f64), recv.clone()],
1721            this_arg.clone(),
1722        )
1723    };
1724    match method {
1725        "every" => {
1726            for (i, v) in elems.iter().enumerate() {
1727                let r = call_cb(i, v)?;
1728                if !with_host(|h| h.truthy(&r)) {
1729                    return Ok(Value::Bool(false));
1730                }
1731            }
1732            Ok(Value::Bool(true))
1733        }
1734        "some" => {
1735            for (i, v) in elems.iter().enumerate() {
1736                let r = call_cb(i, v)?;
1737                if with_host(|h| h.truthy(&r)) {
1738                    return Ok(Value::Bool(true));
1739                }
1740            }
1741            Ok(Value::Bool(false))
1742        }
1743        "forEach" => {
1744            for (i, v) in elems.iter().enumerate() {
1745                call_cb(i, v)?;
1746            }
1747            Ok(Value::Undef)
1748        }
1749        "map" => {
1750            let mut out = Vec::with_capacity(elems.len());
1751            for (i, v) in elems.iter().enumerate() {
1752                let r = call_cb(i, v)?;
1753                out.push(coerce_val(&kind, &r)?);
1754            }
1755            Ok(species(recv, &kind, out))
1756        }
1757        "filter" => {
1758            let mut out = Vec::new();
1759            for (i, v) in elems.iter().enumerate() {
1760                let r = call_cb(i, v)?;
1761                if with_host(|h| h.truthy(&r)) {
1762                    out.push(v.clone());
1763                }
1764            }
1765            Ok(species(recv, &kind, out))
1766        }
1767        "find" | "findIndex" | "findLast" | "findLastIndex" => {
1768            let last = method.starts_with("findLast");
1769            let idxs: Vec<usize> = if last {
1770                (0..elems.len()).rev().collect()
1771            } else {
1772                (0..elems.len()).collect()
1773            };
1774            for i in idxs {
1775                let r = call_cb(i, &elems[i])?;
1776                if with_host(|h| h.truthy(&r)) {
1777                    return Ok(if method.ends_with("Index") {
1778                        Value::Float(i as f64)
1779                    } else {
1780                        elems[i].clone()
1781                    });
1782                }
1783            }
1784            Ok(if method.ends_with("Index") {
1785                Value::Float(-1.0)
1786            } else {
1787                Value::Undef
1788            })
1789        }
1790        "reduce" | "reduceRight" => {
1791            let right = method == "reduceRight";
1792            let order: Vec<usize> = if right {
1793                (0..elems.len()).rev().collect()
1794            } else {
1795                (0..elems.len()).collect()
1796            };
1797            let cb = args.first().cloned().unwrap_or(Value::Undef);
1798            let mut it = order.into_iter();
1799            let mut acc = if args.len() >= 2 {
1800                args[1].clone()
1801            } else {
1802                match it.next() {
1803                    Some(i) => elems[i].clone(),
1804                    None => {
1805                        return Err(crate::host::type_error(
1806                            "Reduce of empty array with no initial value",
1807                        ))
1808                    }
1809                }
1810            };
1811            for i in it {
1812                acc = crate::host::invoke(
1813                    &cb,
1814                    vec![acc, elems[i].clone(), Value::Float(i as f64), recv.clone()],
1815                    None,
1816                )?;
1817            }
1818            Ok(acc)
1819        }
1820        "reverse" => {
1821            let mut out = elems.clone();
1822            out.reverse();
1823            write_elems(recv, &kind, &out)?;
1824            Ok(recv.clone())
1825        }
1826        "sort" => {
1827            let mut out = elems.clone();
1828            sort_elements(&mut out, &kind, args.first())?;
1829            write_elems(recv, &kind, &out)?;
1830            Ok(recv.clone())
1831        }
1832        "copyWithin" => {
1833            let len = elems.len();
1834            let target = rel_index(args, 0, len, 0);
1835            let start = rel_index(args, 1, len, 0);
1836            let end = rel_index(args, 2, len, len);
1837            let src: Vec<Value> = elems[start.min(end)..end.max(start)].to_vec();
1838            let mut out = elems.clone();
1839            for (k, v) in src.iter().enumerate() {
1840                if target + k < len {
1841                    out[target + k] = v.clone();
1842                }
1843            }
1844            write_elems(recv, &kind, &out)?;
1845            Ok(recv.clone())
1846        }
1847        "at" => {
1848            let n = super::arg_num(args, 0);
1849            let i = if n < 0.0 { elems.len() as f64 + n } else { n };
1850            if i < 0.0 || i >= elems.len() as f64 {
1851                return Ok(Value::Undef);
1852            }
1853            Ok(elems[i as usize].clone())
1854        }
1855        "lastIndexOf" => {
1856            let needle = args.first().cloned().unwrap_or(Value::Undef);
1857            let from = (args.len() > 1).then(|| super::arg_num(args, 1));
1858            let found = crate::builtins::search_start_last(from, elems.len()).and_then(|start| {
1859                elems[..=start]
1860                    .iter()
1861                    .rposition(|x| same_element(x, &needle, false))
1862            });
1863            Ok(Value::Float(found.map(|p| p as f64).unwrap_or(-1.0)))
1864        }
1865        // `%TypedArray%.prototype[Symbol.iterator]` IS `values` (23.2.3.35), so
1866        // it dispatches here rather than reporting itself missing:
1867        // `Uint8Array.prototype[Symbol.iterator].call(ta)` threw
1868        // `@@iterator is not a function`.
1869        "keys" | "values" | "entries" | "@@iterator" => {
1870            let items: Vec<Value> = with_host(|h| match method {
1871                "keys" => (0..elems.len()).map(|i| Value::Float(i as f64)).collect(),
1872                "values" | "@@iterator" => elems.clone(),
1873                _ => elems
1874                    .iter()
1875                    .enumerate()
1876                    .map(|(i, v)| h.new_array(vec![Value::Float(i as f64), v.clone()]))
1877                    .collect(),
1878            });
1879            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
1880        }
1881        "toString" | "join" => {
1882            let sep = if method == "join" && !args.is_empty() {
1883                super::arg_str(args, 0)
1884            } else {
1885                ",".into()
1886            };
1887            let parts: Vec<String> = with_host(|h| elems.iter().map(|n| h.str_of(n)).collect());
1888            Ok(with_host(|h| h.new_str(parts.join(&sep))))
1889        }
1890        "slice" | "subarray" => {
1891            let len = elems.len();
1892            let norm = |n: f64| -> usize {
1893                if n < 0.0 {
1894                    (len as f64 + n).max(0.0) as usize
1895                } else {
1896                    (n as usize).min(len)
1897                }
1898            };
1899            let s = if args.is_empty() {
1900                0
1901            } else {
1902                norm(super::arg_num(args, 0))
1903            };
1904            let e = if args.len() < 2 {
1905                len
1906            } else {
1907                norm(super::arg_num(args, 1))
1908            };
1909            let (lo, hi) = (s.min(e), e.max(s));
1910            // 23.2.3.30: `subarray` is a VIEW over the same buffer — writes
1911            // through it are seen by the original. `slice` copies (23.2.3.27).
1912            if method == "subarray" && super::native_tag(recv).as_deref() == Some("TypedArray") {
1913                if let Some((buf, off)) = view_base(recv) {
1914                    let bpe = bytes_per_element(&kind);
1915                    return Ok(make_view(&kind, &buf, off + lo * bpe, hi - lo));
1916                }
1917            }
1918            Ok(species(recv, &kind, elems[lo..hi].to_vec()))
1919        }
1920        "indexOf" => {
1921            let needle = args.first().cloned().unwrap_or(Value::Undef);
1922            let start = crate::builtins::search_start(super::arg_num(args, 1), elems.len());
1923            Ok(Value::Float(
1924                elems
1925                    .iter()
1926                    .skip(start)
1927                    .position(|x| same_element(x, &needle, false))
1928                    .map(|p| (p + start) as f64)
1929                    .unwrap_or(-1.0),
1930            ))
1931        }
1932        "includes" => {
1933            let needle = args.first().cloned().unwrap_or(Value::Undef);
1934            let start = crate::builtins::search_start(super::arg_num(args, 1), elems.len());
1935            Ok(Value::Bool(
1936                elems
1937                    .iter()
1938                    .skip(start)
1939                    .any(|x| same_element(x, &needle, true)),
1940            ))
1941        }
1942        // 23.2.3.9: `fill` writes THROUGH the view and answers the receiver. It
1943        // was building a fresh array instead, so the write was invisible —
1944        // `u.fill(9)` left `u` untouched, `u.fill(9) === u` was false, and a
1945        // second view onto the same `ArrayBuffer` saw none of it. The `start`
1946        // and `end` arguments were dropped too, so `fill(9, 1, 2)` overwrote the
1947        // whole array rather than one element.
1948        "fill" => {
1949            let len = elems.len();
1950            let v = coerce_val(&kind, args.first().unwrap_or(&Value::Undef))?;
1951            let start = rel_index(args, 1, len, 0);
1952            let end = rel_index(args, 2, len, len);
1953            let mut out = elems.clone();
1954            for slot in out.iter_mut().take(end).skip(start) {
1955                *slot = v.clone();
1956            }
1957            write_elems(recv, &kind, &out)?;
1958            Ok(recv.clone())
1959        }
1960        // The change-by-copy trio (23.2.3.32-34). Each answers a NEW view of the
1961        // receiver's own element kind — `TypedArrayCreateSameType`, not the
1962        // species path — so a `Buffer` receiver yields a `Uint8Array`, which is
1963        // what node reports.
1964        "toReversed" | "toSorted" => {
1965            let mut out = elems.clone();
1966            if method == "toReversed" {
1967                out.reverse();
1968            } else {
1969                sort_elements(&mut out, &kind, args.first())?;
1970            }
1971            Ok(make(&kind, out))
1972        }
1973        "with" => {
1974            let len = elems.len();
1975            let n = super::arg_num(args, 0);
1976            let i = if n < 0.0 { len as f64 + n } else { n };
1977            if !(0.0..len as f64).contains(&i) {
1978                return Err("RangeError: Invalid typed array index".into());
1979            }
1980            let mut out = elems.clone();
1981            out[i as usize] = coerce_val(&kind, args.get(1).unwrap_or(&Value::Undef))?;
1982            Ok(make(&kind, out))
1983        }
1984        "set" => {
1985            // `ta.set(src[, offset])` — write `src`'s values in place.
1986            let arg = args.first().cloned().unwrap_or(Value::Undef);
1987            let src = match super::native_tag(&arg).as_deref() {
1988                Some("TypedArray") | Some("Buffer") => elem_values(&arg),
1989                _ => crate::host::iter_all(&arg)
1990                    .unwrap_or_else(|_| crate::builtins::array_like_items(&arg)),
1991            };
1992            // 23.2.3.26: a negative offset, or a source that runs past the
1993            // end, is a RangeError. Neither may write a partial prefix.
1994            let off = super::arg_num(args, 1);
1995            let off = if off.is_nan() { 0.0 } else { off.trunc() };
1996            if off < 0.0 || off + src.len() as f64 > view_len(recv) as f64 {
1997                return Err(crate::host::range_error("offset is out of bounds"));
1998            }
1999            let off = off as usize;
2000            // Coerced outside the host borrow: a 64-bit element allocates.
2001            let src: Vec<Value> = src
2002                .iter()
2003                .map(|v| coerce_val(&kind, v))
2004                .collect::<Result<_, _>>()?;
2005            let bpe = bytes_per_element(&kind);
2006            let len = view_len(recv);
2007            for (k, v) in src.into_iter().enumerate() {
2008                if off + k < len {
2009                    write_view_bytes(recv, (off + k) * bpe, &encode(&kind, &v));
2010                }
2011            }
2012            Ok(Value::Undef)
2013        }
2014        _ => Err(crate::host::type_error(&format!(
2015            "{method} is not a function"
2016        ))),
2017    }
2018}
2019
2020// ── WeakRef (strong-ref approximation) ────────────────────────────────────────
2021
2022pub fn construct_weakref(args: &[Value]) -> Result<Value, String> {
2023    let target = args.first().cloned().unwrap_or(Value::Undef);
2024    Ok(with_host(|h| {
2025        let mut m = IndexMap::new();
2026        m.insert("@@native".into(), h.new_str("WeakRef"));
2027        m.insert("@@target".into(), target);
2028        h.new_object(m)
2029    }))
2030}
2031
2032pub fn weakref_call(recv: &Value, method: &str) -> Result<Value, String> {
2033    match method {
2034        "deref" => Ok(with_host(|h| match h.get(recv) {
2035            Some(JsObj::Object(p)) => p.get("@@target").cloned().unwrap_or(Value::Undef),
2036            _ => Value::Undef,
2037        })),
2038        _ => Err(crate::host::type_error(&format!(
2039            "{method} is not a function"
2040        ))),
2041    }
2042}
2043
2044// ── FinalizationRegistry (no-GC approximation) ────────────────────────────────
2045//
2046// This VM holds every value strongly (see `WeakRef` above), so a registered
2047// target is never reclaimed and the cleanup callback never fires. The ECMAScript
2048// spec permits an implementation to never call cleanup callbacks, so this is a
2049// conformant approximation: the constructor and `register`/`unregister` enforce
2050// their type checks and `unregister`'s bookkeeping exactly, only the (optional)
2051// callback invocation is absent. Registered unregister-tokens are tracked in a
2052// hidden `@@fr_tokens` array so `unregister` returns the correct boolean.
2053
2054/// Whether `v` is an Object (a valid `register` target / unregister token) — a
2055/// heap value that is not one of the primitive-wrapper heap variants.
2056fn is_object_value(v: &Value) -> bool {
2057    matches!(v, Value::Obj(_))
2058        && with_host(|h| {
2059            !matches!(
2060                h.get(v),
2061                Some(JsObj::Str(_))
2062                    | Some(JsObj::Symbol { .. })
2063                    | Some(JsObj::BigInt(_))
2064                    | Some(JsObj::Null)
2065            )
2066        })
2067}
2068
2069pub fn construct_finalization_registry(args: &[Value]) -> Result<Value, String> {
2070    let cb = args.first().cloned().unwrap_or(Value::Undef);
2071    if !with_host(|h| crate::host::is_callable(h, &cb)) {
2072        return Err(crate::host::type_error(
2073            "FinalizationRegistry: cleanup must be callable",
2074        ));
2075    }
2076    Ok(with_host(|h| {
2077        let tokens = h.new_array(Vec::new());
2078        let mut m = IndexMap::new();
2079        m.insert("@@native".into(), h.new_str("FinalizationRegistry"));
2080        m.insert("@@fr_cb".into(), cb);
2081        m.insert("@@fr_tokens".into(), tokens);
2082        h.new_object(m)
2083    }))
2084}
2085
2086pub fn finalization_registry_call(
2087    recv: &Value,
2088    method: &str,
2089    args: &[Value],
2090) -> Result<Value, String> {
2091    match method {
2092        "register" => {
2093            let target = args.first().cloned().unwrap_or(Value::Undef);
2094            let held = args.get(1).cloned().unwrap_or(Value::Undef);
2095            let token = args.get(2).cloned().unwrap_or(Value::Undef);
2096            if !is_object_value(&target) {
2097                // V8's wording is `invalid target`; the "must be an object"
2098                // phrasing was this file's own, not any engine's.
2099                return Err(crate::host::type_error(
2100                    "FinalizationRegistry.prototype.register: invalid target",
2101                ));
2102            }
2103            if with_host(|h| h.strict_eq(&target, &held)) {
2104                return Err(crate::host::type_error(
2105                    "FinalizationRegistry.prototype.register: target and holdings must not be same",
2106                ));
2107            }
2108            // A supplied unregister token must be an object; record it so a later
2109            // `unregister` can find (and drop) this registration.
2110            if !matches!(token, Value::Undef) {
2111                if !is_object_value(&token) {
2112                    return Err(crate::host::type_error(&format!(
2113                        "Invalid unregisterToken ('{}')",
2114                        with_host(|h| h.str_of(&token))
2115                    )));
2116                }
2117                with_host(|h| {
2118                    let toks = registry_tokens(h, recv);
2119                    if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
2120                        items.push(token);
2121                    }
2122                });
2123            }
2124            Ok(Value::Undef)
2125        }
2126        "unregister" => {
2127            let token = args.first().cloned().unwrap_or(Value::Undef);
2128            if !is_object_value(&token) {
2129                // V8 names the token and does not mention the method.
2130                return Err(crate::host::type_error(&format!(
2131                    "Invalid unregisterToken ('{}')",
2132                    with_host(|h| h.str_of(&token))
2133                )));
2134            }
2135            Ok(Value::Bool(with_host(|h| {
2136                let toks = registry_tokens(h, recv);
2137                let kept: Vec<Value> = match h.get(&toks) {
2138                    Some(JsObj::Array(items)) => items
2139                        .iter()
2140                        .filter(|t| !h.strict_eq(t, &token))
2141                        .cloned()
2142                        .collect(),
2143                    _ => Vec::new(),
2144                };
2145                let removed = match h.get(&toks) {
2146                    Some(JsObj::Array(items)) => items.len() != kept.len(),
2147                    _ => false,
2148                };
2149                if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
2150                    *items = kept;
2151                }
2152                removed
2153            })))
2154        }
2155        _ => Err(crate::host::type_error(&format!(
2156            "{method} is not a function"
2157        ))),
2158    }
2159}
2160
2161/// The hidden `@@fr_tokens` array backing a `FinalizationRegistry`.
2162fn registry_tokens(h: &crate::host::JsHost, recv: &Value) -> Value {
2163    match h.get(recv) {
2164        Some(JsObj::Object(p)) => p.get("@@fr_tokens").cloned().unwrap_or(Value::Undef),
2165        _ => Value::Undef,
2166    }
2167}
2168
2169// ── TextEncoder / TextDecoder ─────────────────────────────────────────────────
2170
2171pub fn construct_text_encoder() -> Result<Value, String> {
2172    Ok(with_host(|h| {
2173        let mut m = IndexMap::new();
2174        m.insert("@@native".into(), h.new_str("TextEncoder"));
2175        // `encoding` is a getter on the prototype, so the value lives in the
2176        // hidden slot the getter reads. As an own property it enumerated —
2177        // `Object.keys(new TextEncoder())` answered `["encoding"]` where node
2178        // answers `[]`, and `JSON.stringify` of anything holding one carried it.
2179        m.insert("@@encoding".into(), h.new_str("utf-8"));
2180        h.new_object(m)
2181    }))
2182}
2183
2184pub fn text_encoder_call(_recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
2185    match method {
2186        // `encode(str)` → a Uint8Array of the UTF-8 bytes.
2187        "encode" => {
2188            let s = super::arg_str(args, 0);
2189            Ok(make(
2190                "Uint8Array",
2191                s.as_bytes()
2192                    .iter()
2193                    .map(|b| Value::Float(*b as f64))
2194                    .collect(),
2195            ))
2196        }
2197        _ => Err(crate::host::type_error(&format!(
2198            "{method} is not a function"
2199        ))),
2200    }
2201}
2202
2203/// The WHATWG encoding a label names, as node reports it through
2204/// `decoder.encoding`. The label is NOT the encoding: `latin1`, `ascii` and
2205/// `iso-8859-1` all name `windows-1252`, and `ucs-2` names `utf-16le`. This
2206/// echoed the label back, so `new TextDecoder("latin1").encoding` read
2207/// `"latin1"` — a name node never reports — and an unknown label was accepted
2208/// and then decoded as UTF-8.
2209fn encoding_for_label(label: &str) -> Option<&'static str> {
2210    Some(match label.trim().to_ascii_lowercase().as_str() {
2211        "utf-8" | "utf8" | "unicode-1-1-utf-8" | "unicode11utf8" | "unicode20utf8"
2212        | "x-unicode20utf8" => "utf-8",
2213        "latin1" | "iso-8859-1" | "iso8859-1" | "iso88591" | "ascii" | "us-ascii" | "cp1252"
2214        | "cp819" | "ibm819" | "l1" | "windows-1252" | "x-cp1252" => "windows-1252",
2215        "utf-16le" | "utf-16" | "ucs-2" | "ucs2" | "unicodefeff" | "unicodefffe"
2216        | "iso-10646-ucs-2" | "csunicode" => "utf-16le",
2217        _ => return None,
2218    })
2219}
2220
2221pub fn construct_text_decoder(args: &[Value]) -> Result<Value, String> {
2222    let label = if args.is_empty() || matches!(args[0], Value::Undef) {
2223        "utf-8".to_string()
2224    } else {
2225        super::arg_str(args, 0)
2226    };
2227    let Some(encoding) = encoding_for_label(&label) else {
2228        return Err(crate::host::coded_error(
2229            "RangeError",
2230            "ERR_ENCODING_NOT_SUPPORTED",
2231            &format!("The \"{label}\" encoding is not supported"),
2232        ));
2233    };
2234    // `fatal` and `ignoreBOM` were not read at all, so a decoder asked to reject
2235    // malformed input accepted it and one asked to keep the BOM never saw one —
2236    // both options silently did nothing.
2237    let flag = |key: &str| {
2238        args.get(1)
2239            .map(|o| crate::builtins::get_property(o, key).unwrap_or(Value::Undef))
2240            .map(|v| with_host(|h| h.truthy(&v)))
2241            .unwrap_or(false)
2242    };
2243    let (fatal, ignore_bom) = (flag("fatal"), flag("ignoreBOM"));
2244    Ok(with_host(|h| {
2245        let mut m = IndexMap::new();
2246        m.insert("@@native".into(), h.new_str("TextDecoder"));
2247        m.insert("@@encoding".into(), h.new_str(encoding.to_string()));
2248        m.insert("@@fatal".into(), Value::Bool(fatal));
2249        m.insert("@@ignoreBOM".into(), Value::Bool(ignore_bom));
2250        h.new_object(m)
2251    }))
2252}
2253
2254/// windows-1252's 0x80..=0x9F range, which is NOT latin1's: those 32 positions
2255/// carry the typographic characters (curly quotes, the euro sign, the dashes)
2256/// rather than C1 control codes. Decoding them as latin1 — `b as char`, what
2257/// this did — turned every smart quote in a Windows-encoded file into a control
2258/// character.
2259const CP1252_HIGH: [char; 32] = [
2260    '\u{20ac}', '\u{81}', '\u{201a}', '\u{192}', '\u{201e}', '\u{2026}', '\u{2020}', '\u{2021}',
2261    '\u{2c6}', '\u{2030}', '\u{160}', '\u{2039}', '\u{152}', '\u{8d}', '\u{17d}', '\u{8f}',
2262    '\u{90}', '\u{2018}', '\u{2019}', '\u{201c}', '\u{201d}', '\u{2022}', '\u{2013}', '\u{2014}',
2263    '\u{2dc}', '\u{2122}', '\u{161}', '\u{203a}', '\u{153}', '\u{9d}', '\u{17e}', '\u{178}',
2264];
2265
2266pub fn text_decoder_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
2267    match method {
2268        // `decode(bytes)` → a string from the buffer's UTF-8 (or latin1) bytes.
2269        "decode" => {
2270            let bytes: Vec<u8> = elems_of(&args.first().cloned().unwrap_or(Value::Undef))
2271                .unwrap_or_default()
2272                .iter()
2273                .map(|n| *n as u8)
2274                .collect();
2275            let slot = |key: &str| {
2276                with_host(|h| match h.get(recv) {
2277                    Some(JsObj::Object(p)) => p.get(key).cloned(),
2278                    _ => None,
2279                })
2280            };
2281            let enc = slot("@@encoding")
2282                .map(|v| with_host(|h| h.str_of(&v)))
2283                .unwrap_or_else(|| "utf-8".into());
2284            let flag = |key: &str| matches!(slot(key), Some(Value::Bool(true)));
2285            let s = match enc.as_str() {
2286                "windows-1252" => bytes
2287                    .iter()
2288                    .map(|b| match b {
2289                        0x80..=0x9f => CP1252_HIGH[(b - 0x80) as usize],
2290                        _ => *b as char,
2291                    })
2292                    .collect(),
2293                "utf-16le" => {
2294                    let units: Vec<u16> = bytes
2295                        .chunks_exact(2)
2296                        .map(|c| u16::from_le_bytes([c[0], c[1]]))
2297                        .collect();
2298                    String::from_utf16_lossy(&units)
2299                }
2300                // A `fatal` decoder REJECTS malformed input rather than
2301                // substituting U+FFFD. Both behaved as the lossy form, so bytes
2302                // that are not valid UTF-8 came back as replacement characters
2303                // from a decoder built to refuse them.
2304                _ if flag("@@fatal") => match std::str::from_utf8(&bytes) {
2305                    Ok(s) => s.to_string(),
2306                    Err(_) => {
2307                        return Err(crate::host::coded_error(
2308                            "TypeError",
2309                            "ERR_ENCODING_INVALID_ENCODED_DATA",
2310                            &format!("The encoded data was not valid for encoding {enc}"),
2311                        ))
2312                    }
2313                },
2314                _ => String::from_utf8_lossy(&bytes).into_owned(),
2315            };
2316            // A leading BOM is REMOVED unless `ignoreBOM` asked to keep it.
2317            let s = match s.strip_prefix('\u{feff}') {
2318                Some(rest) if !flag("@@ignoreBOM") => rest.to_string(),
2319                _ => s,
2320            };
2321            Ok(with_host(|h| h.new_str(s)))
2322        }
2323        _ => Err(crate::host::type_error(&format!(
2324            "{method} is not a function"
2325        ))),
2326    }
2327}