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).unwrap_or_else(|_| crate::builtins::array_like_items(&src))
984    };
985    let mut out = Vec::with_capacity(items.len());
986    for (i, it) in items.into_iter().enumerate() {
987        let mapped = match &map_fn {
988            Some(f) => crate::host::invoke(f, vec![it, Value::Float(i as f64)], None)?,
989            None => it,
990        };
991        out.push(coerce_val(kind, &mapped)?);
992    }
993    Ok(make(kind, out))
994}
995
996/// The element values of a typed array / Buffer (`None` for anything else).
997pub fn elems_of(v: &Value) -> Option<Vec<f64>> {
998    let tag = super::native_tag(v)?;
999    if !matches!(tag.as_str(), "TypedArray" | "Buffer") {
1000        return None;
1001    }
1002    let vals = elem_values(v);
1003    Some(with_host(|h| vals.iter().map(|x| h.to_number(x)).collect()))
1004}
1005
1006/// The number of elements `v` exposes as integer-index own properties, for a
1007/// typed array (its view length) or a `Buffer` (`@@bytes`); `None` otherwise.
1008///
1009/// Both index-membership questions — `obj.hasOwnProperty(i)` and `i in obj` —
1010/// must answer from this one place. They used to disagree: `hasOwnProperty`
1011/// carried a hand-rolled arm that understood `@@bytes` only, so it was right for
1012/// a Buffer and wrong for every other typed array, while the `in` operator knew
1013/// about neither and reported false for every valid index of both.
1014pub fn index_len(v: &Value) -> Option<usize> {
1015    match super::native_tag(v)?.as_str() {
1016        "TypedArray" => Some(view_len(v)),
1017        "Buffer" => with_host(|h| match h.get(v) {
1018            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
1019                Some(JsObj::Array(items)) => Some(items.len()),
1020                _ => None,
1021            },
1022            _ => None,
1023        }),
1024        _ => None,
1025    }
1026}
1027
1028/// Whether `key` is an in-range integer index of the typed array / Buffer `v`.
1029/// `None` when `v` is neither, so callers can fall through to their own logic.
1030pub fn has_index(v: &Value, key: &str) -> Option<bool> {
1031    let len = index_len(v)?;
1032    Some(key.parse::<usize>().map(|i| i < len).unwrap_or(false))
1033}
1034
1035/// The `@@kind` of a typed-array receiver (defaults to `Uint8Array`).
1036pub fn kind_of(recv: &Value) -> String {
1037    with_host(|h| match h.get(recv) {
1038        Some(JsObj::Object(p)) => p
1039            .get("@@kind")
1040            .map(|v| h.str_of(v))
1041            .unwrap_or_else(|| "Uint8Array".into()),
1042        _ => "Uint8Array".into(),
1043    })
1044}
1045
1046// ── backing store ────────────────────────────────────────────────────────────
1047//
1048// Every view — typed array or `DataView` — reads and writes THROUGH an
1049// `ArrayBuffer`, which owns the only copy of the bytes as a hidden `@@bytes`
1050// heap array. That is what makes two views over one buffer see each other's
1051// writes: `new Uint32Array(ab)[0]` reflects a byte written through
1052// `new Uint8Array(ab)`. Before this an `ArrayBuffer` carried nothing but a
1053// `byteLength` and each view owned a private element vector, so nothing was
1054// ever shared and `DataView` did not exist at all.
1055
1056/// Allocate an `ArrayBuffer` of `n` zeroed bytes.
1057pub fn new_array_buffer(n: usize) -> Value {
1058    with_host(|h| {
1059        let arr = h.new_array(vec![Value::Float(0.0); n]);
1060        let mut m = IndexMap::new();
1061        m.insert("@@native".into(), h.new_str("ArrayBuffer"));
1062        m.insert("@@bytes".into(), arr);
1063        m.insert("byteLength".into(), Value::Float(n as f64));
1064        // `detached` is a prototype accessor in the spec; kept as a hidden own
1065        // property here so it reads back without appearing in `Object.keys` or
1066        // `console.log`, the same way `byteLength` is.
1067        m.insert("detached".into(), Value::Bool(false));
1068        // A FIXED buffer still reports both, as `false` and its own length —
1069        // they are prototype accessors in the spec, so they always answer.
1070        m.insert("resizable".into(), Value::Bool(false));
1071        m.insert("maxByteLength".into(), Value::Float(n as f64));
1072        let obj = h.new_object(m);
1073        for k in ["byteLength", "detached", "resizable", "maxByteLength"] {
1074            h.hide_prop(&obj, k);
1075        }
1076        // `ensure_ctor_proto` builds the prototype WITH a `constructor` slot, so
1077        // `ab.constructor.name` reports `ArrayBuffer` rather than `Object`.
1078        if let Some(p) = h.ensure_ctor_proto("ArrayBuffer") {
1079            h.set_proto(&obj, p);
1080        }
1081        obj
1082    })
1083}
1084
1085/// Whether `ab` has been DETACHED — its bytes handed to another buffer by
1086/// `transfer`, or given away by `structuredClone`'s `transfer` option.
1087///
1088/// A detached buffer is not an empty one: reading a view over it answers
1089/// `undefined` and its `length` is 0, but every METHOD on that view throws.
1090pub fn is_detached(ab: &Value) -> bool {
1091    with_host(|h| match h.get(ab) {
1092        Some(JsObj::Object(p)) => p.get("detached").map(|v| h.truthy(v)).unwrap_or(false),
1093        _ => false,
1094    })
1095}
1096
1097/// Whether `v` is a view whose backing buffer has been detached.
1098pub fn view_detached(v: &Value) -> bool {
1099    with_host(|h| view_detached_h(h, v))
1100}
1101
1102/// `view_detached` for a caller that already holds the host borrow — the
1103/// iteration entry point runs under one, and re-entering aborts the process.
1104pub fn view_detached_h(h: &crate::host::JsHost, v: &Value) -> bool {
1105    let buf = match h.get(v) {
1106        Some(JsObj::Object(p)) => p.get("@@buffer").cloned(),
1107        _ => None,
1108    };
1109    match buf.and_then(|b| match h.get(&b) {
1110        Some(JsObj::Object(p)) => p.get("detached").cloned(),
1111        _ => None,
1112    }) {
1113        Some(d) => h.truthy(&d),
1114        None => false,
1115    }
1116}
1117
1118/// Detach `ab`: drop its bytes and mark it, so every later read reports zero
1119/// length and every method over it throws.
1120pub fn detach_buffer(ab: &Value) {
1121    detach(ab)
1122}
1123
1124fn detach(ab: &Value) {
1125    with_host(|h| {
1126        let empty = h.new_array(Vec::new());
1127        if let Some(JsObj::Object(p)) = h.get_mut(ab) {
1128            p.insert("@@bytes".into(), empty);
1129            p.insert("byteLength".into(), Value::Float(0.0));
1130            p.insert("detached".into(), Value::Bool(true));
1131        }
1132        h.hide_prop(ab, "byteLength");
1133        h.hide_prop(ab, "detached");
1134    });
1135}
1136
1137/// `ArrayBuffer.prototype.transfer([newLength])` and `transferToFixedLength`.
1138///
1139/// A fresh buffer takes the bytes — truncated or zero-padded to `newLength` —
1140/// and the receiver is detached. The two differ only in whether the result may
1141/// still grow.
1142pub fn buffer_transfer(ab: &Value, args: &[Value], fixed: bool) -> Result<Value, String> {
1143    let method = if fixed {
1144        "transferToFixedLength"
1145    } else {
1146        "transfer"
1147    };
1148    if is_detached(ab) {
1149        return Err(crate::host::type_error(&format!(
1150            "Cannot perform ArrayBuffer.prototype.{method} on a detached ArrayBuffer"
1151        )));
1152    }
1153    let old = byte_len_of(ab);
1154    let new_len = match args.first().filter(|v| !matches!(v, Value::Undef)) {
1155        Some(v) => with_host(|h| h.to_number(v)).max(0.0) as usize,
1156        None => old,
1157    };
1158    let mut bytes = view_bytes_of_buffer(ab, old);
1159    bytes.resize(new_len, 0);
1160    let out = new_array_buffer(new_len);
1161    write_buffer_bytes(&out, &bytes);
1162    if !fixed {
1163        // `transfer` keeps the source's resizability; `transferToFixedLength`
1164        // never does.
1165        let resizable = with_host(|h| match h.get(ab) {
1166            Some(JsObj::Object(p)) => p.contains_key("@@maxByteLength"),
1167            _ => false,
1168        });
1169        if resizable {
1170            let max = with_host(|h| match h.get(ab) {
1171                Some(JsObj::Object(p)) => p.get("@@maxByteLength").cloned(),
1172                _ => None,
1173            });
1174            if let Some(max) = max {
1175                with_host(|h| {
1176                    if let Some(JsObj::Object(p)) = h.get_mut(&out) {
1177                        p.insert("@@maxByteLength".into(), max);
1178                    }
1179                });
1180            }
1181        }
1182    }
1183    detach(ab);
1184    Ok(out)
1185}
1186
1187/// An ArrayBuffer's `byteLength`.
1188fn byte_len_of(ab: &Value) -> usize {
1189    with_host(|h| match h.get(ab) {
1190        Some(JsObj::Object(p)) => {
1191            p.get("byteLength").map(|l| h.to_number(l)).unwrap_or(0.0) as usize
1192        }
1193        _ => 0,
1194    })
1195}
1196
1197/// An ArrayBuffer's bytes.
1198fn view_bytes_of_buffer(ab: &Value, n: usize) -> Vec<u8> {
1199    let Some(store) = store_of(ab) else {
1200        return Vec::new();
1201    };
1202    with_host(|h| match h.get(&store) {
1203        Some(JsObj::Array(items)) => items
1204            .iter()
1205            .take(n)
1206            .map(|x| h.to_number(x) as i64 as u8)
1207            .collect(),
1208        _ => Vec::new(),
1209    })
1210}
1211
1212/// The TypeError a method over a DETACHED buffer throws. Node names the method
1213/// and distinguishes a view's from a DataView's from the buffer's own.
1214pub fn detached_error(label: &str, method: &str, buffer_only: bool) -> String {
1215    let tail = if buffer_only {
1216        "a detached ArrayBuffer"
1217    } else {
1218        "a detached or out-of-bounds ArrayBuffer"
1219    };
1220    // A symbol-keyed member reports the name of the function it ALIASES, the way
1221    // node does everywhere else (`Set.prototype.keys` reports `values`):
1222    // `[...detachedView]` says `%TypedArray%.prototype.values`, never
1223    // `.@@iterator`, which is this frontend's internal spelling for
1224    // `Symbol.iterator` and not a name any script wrote.
1225    let method = match method {
1226        "@@iterator" => "values",
1227        other => other,
1228    };
1229    crate::host::type_error(&format!("Cannot perform {label}.{method} on {tail}"))
1230}
1231
1232/// The heap array holding an `ArrayBuffer`'s bytes.
1233fn store_of(ab: &Value) -> Option<Value> {
1234    with_host(|h| match h.get(ab) {
1235        Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
1236        _ => None,
1237    })
1238}
1239
1240/// A view's `(buffer, byteOffset)`.
1241fn view_base(v: &Value) -> Option<(Value, usize)> {
1242    with_host(|h| match h.get(v) {
1243        Some(JsObj::Object(p)) => {
1244            let buf = p.get("@@buffer").cloned()?;
1245            let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0);
1246            Some((buf, off.max(0.0) as usize))
1247        }
1248        _ => None,
1249    })
1250}
1251
1252/// `n` bytes of `v`'s buffer starting at its `byteOffset + at`.
1253pub fn view_bytes(v: &Value, at: usize, n: usize) -> Option<Vec<u8>> {
1254    let (buf, off) = view_base(v)?;
1255    let store = store_of(&buf)?;
1256    with_host(|h| match h.get(&store) {
1257        Some(JsObj::Array(items)) => {
1258            let start = off + at;
1259            if start + n > items.len() {
1260                return None;
1261            }
1262            Some(
1263                items[start..start + n]
1264                    .iter()
1265                    .map(|x| h.to_number(x) as i64 as u8)
1266                    .collect(),
1267            )
1268        }
1269        _ => None,
1270    })
1271}
1272
1273/// Write `bytes` into `v`'s buffer at its `byteOffset + at`. False when the
1274/// range does not fit.
1275pub fn write_view_bytes(v: &Value, at: usize, bytes: &[u8]) -> bool {
1276    let Some((buf, off)) = view_base(v) else {
1277        return false;
1278    };
1279    let Some(store) = store_of(&buf) else {
1280        return false;
1281    };
1282    with_host(|h| match h.get_mut(&store) {
1283        Some(JsObj::Array(items)) => {
1284            let start = off + at;
1285            if start + bytes.len() > items.len() {
1286                return false;
1287            }
1288            for (i, b) in bytes.iter().enumerate() {
1289                items[start + i] = Value::Float(*b as f64);
1290            }
1291            true
1292        }
1293        _ => false,
1294    })
1295}
1296
1297/// Decode one element of `kind` from its `bytes` (native byte order, which on
1298/// every architecture this runs on is little-endian).
1299fn decode(kind: &str, b: &[u8]) -> Value {
1300    match kind {
1301        "Int8Array" => Value::Float(b[0] as i8 as f64),
1302        "Uint8Array" | "Uint8ClampedArray" => Value::Float(b[0] as f64),
1303        "Int16Array" => Value::Float(i16::from_le_bytes([b[0], b[1]]) as f64),
1304        "Uint16Array" => Value::Float(u16::from_le_bytes([b[0], b[1]]) as f64),
1305        "Int32Array" => Value::Float(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
1306        "Uint32Array" => Value::Float(u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
1307        "Float32Array" => Value::Float(f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
1308        "BigInt64Array" => {
1309            let raw = i64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1310            with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
1311        }
1312        "BigUint64Array" => {
1313            let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1314            with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
1315        }
1316        _ => Value::Float(f64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]))),
1317    }
1318}
1319
1320/// Encode one already-coerced element of `kind` into its bytes.
1321fn encode(kind: &str, v: &Value) -> Vec<u8> {
1322    if is_bigint_kind(kind) {
1323        use num_traits::cast::ToPrimitive;
1324        let b = bigint_of(v);
1325        let raw = if kind == "BigInt64Array" {
1326            b.to_i64().unwrap_or(0) as u64
1327        } else {
1328            b.to_u64().unwrap_or(0)
1329        };
1330        return raw.to_le_bytes().to_vec();
1331    }
1332    let n = num(v);
1333    match kind {
1334        "Int8Array" => vec![n as i64 as i8 as u8],
1335        "Uint8Array" | "Uint8ClampedArray" => vec![n as i64 as u8],
1336        "Int16Array" => (n as i64 as i16).to_le_bytes().to_vec(),
1337        "Uint16Array" => (n as i64 as u16).to_le_bytes().to_vec(),
1338        "Int32Array" => (n as i64 as i32).to_le_bytes().to_vec(),
1339        "Uint32Array" => (n as i64 as u32).to_le_bytes().to_vec(),
1340        "Float32Array" => (n as f32).to_le_bytes().to_vec(),
1341        _ => n.to_le_bytes().to_vec(),
1342    }
1343}
1344
1345/// The elements of a typed-array view, decoded with a host borrow ALREADY
1346/// held. `host` reads views from inside `&self` methods (inspect, key
1347/// enumeration, iteration) where re-entering through `with_host` would panic on
1348/// the outstanding borrow.
1349pub fn elems_with_host(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
1350    if let Some(JsObj::Object(p)) = h.get(v) {
1351        if let Some(arr) = p.get("@@bytes") {
1352            return match h.get(arr) {
1353                Some(JsObj::Array(items)) => items.clone(),
1354                _ => Vec::new(),
1355            };
1356        }
1357    }
1358    let Some((kind, raws)) = raw_elems(h, v) else {
1359        return Vec::new();
1360    };
1361    // A 64-bit element is a BigInt, which needs an allocation this borrow
1362    // cannot make; `elems_mut_host` is the reader for callers that can.
1363    if is_bigint_kind(&kind) {
1364        return vec![Value::Undef; raws.len()];
1365    }
1366    raws.iter().map(|b| decode(&kind, b)).collect()
1367}
1368
1369/// The raw bytes of every element of a view, with the host borrow already held.
1370/// The shared half of the three readers below.
1371fn raw_elems(h: &crate::host::JsHost, v: &Value) -> Option<(String, Vec<Vec<u8>>)> {
1372    let JsObj::Object(p) = h.get(v)? else {
1373        return None;
1374    };
1375    let kind = p
1376        .get("@@kind")
1377        .map(|k| h.str_of(k))
1378        .unwrap_or_else(|| "Uint8Array".into());
1379    let bpe = bytes_per_element(&kind);
1380    // A view over a DETACHED buffer has no elements. Its own `length` still
1381    // holds the old count, so `util.inspect` showed `Uint8Array(4) [0,0,0,0]`
1382    // over a buffer with no bytes left.
1383    let len = if view_detached_h(h, v) {
1384        0
1385    } else {
1386        p.get("length").map(|l| h.to_number(l)).unwrap_or(0.0) as usize
1387    };
1388    let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0) as usize;
1389    let store = match p.get("@@buffer").and_then(|b| h.get(b)) {
1390        Some(JsObj::Object(bp)) => bp.get("@@bytes").and_then(|a| h.get(a)),
1391        _ => None,
1392    };
1393    let JsObj::Array(bytes) = store? else {
1394        return None;
1395    };
1396    let out = (0..len)
1397        .map(|i| {
1398            let start = off + i * bpe;
1399            if start + bpe > bytes.len() {
1400                return vec![0u8; bpe];
1401            }
1402            bytes[start..start + bpe]
1403                .iter()
1404                .map(|x| h.to_number(x) as i64 as u8)
1405                .collect()
1406        })
1407        .collect();
1408    Some((kind, out))
1409}
1410
1411/// The elements of a view with a MUTABLE host borrow held, so the two 64-bit
1412/// kinds can allocate their BigInts. This is the complete reader; the `&self`
1413/// one below cannot allocate and so answers `undefined` for those two kinds.
1414pub fn elems_mut_host(h: &mut crate::host::JsHost, v: &Value) -> Vec<Value> {
1415    if let Some(JsObj::Object(p)) = h.get(v) {
1416        if let Some(arr) = p.get("@@bytes").cloned() {
1417            return match h.get(&arr) {
1418                Some(JsObj::Array(items)) => items.clone(),
1419                _ => Vec::new(),
1420            };
1421        }
1422    }
1423    let Some((kind, raws)) = raw_elems(h, v) else {
1424        return Vec::new();
1425    };
1426    raws.iter()
1427        .map(|b| {
1428            if !is_bigint_kind(&kind) {
1429                return decode(&kind, b);
1430            }
1431            let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1432            h.new_bigint(if kind == "BigInt64Array" {
1433                num_bigint::BigInt::from(raw as i64)
1434            } else {
1435                num_bigint::BigInt::from(raw)
1436            })
1437        })
1438        .collect()
1439}
1440
1441/// Every element rendered for display, for `util.inspect` — which holds a
1442/// shared borrow and so cannot allocate the BigInt a 64-bit element would need
1443/// as a `Value`. Elements are always primitives, so a string loses nothing.
1444pub fn elems_display(h: &crate::host::JsHost, v: &Value) -> Vec<String> {
1445    let Some((kind, raws)) = raw_elems(h, v) else {
1446        return Vec::new();
1447    };
1448    raws.iter()
1449        .map(|b| {
1450            if !is_bigint_kind(&kind) {
1451                return h.inspect(&decode(&kind, b));
1452            }
1453            let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
1454            if kind == "BigInt64Array" {
1455                format!("{}n", raw as i64)
1456            } else {
1457                format!("{raw}n")
1458            }
1459        })
1460        .collect()
1461}
1462
1463/// The element count a view exposes, from its own `length` slot.
1464fn view_len(v: &Value) -> usize {
1465    // A view over a DETACHED buffer has length 0 — its own `length` property
1466    // still holds the old count, which is why this cannot just read it.
1467    if view_detached(v) {
1468        return 0;
1469    }
1470    with_host(|h| match h.get(v) {
1471        Some(JsObj::Object(p)) => p.get("length").map(|l| h.to_number(l)).unwrap_or(0.0) as usize,
1472        _ => 0,
1473    })
1474}
1475
1476// ── element indexing (called from builtins::get_property/set_property) ────────
1477
1478/// `ta[i]` read: the element at char/index `i`, or `None` if `i` is out of range
1479/// or not an integer index.
1480pub fn elem_get(recv: &Value, key: &str) -> Option<Value> {
1481    let i: usize = key.parse().ok()?;
1482    if i >= view_len(recv) {
1483        return None;
1484    }
1485    let kind = kind_of(recv);
1486    let bpe = bytes_per_element(&kind);
1487    let bytes = view_bytes(recv, i * bpe, bpe)?;
1488    Some(decode(&kind, &bytes))
1489}
1490
1491/// `ta[i] = v` write (coerced to the kind). Returns true if `i` is a valid index.
1492pub fn elem_set(recv: &Value, key: &str, val: &Value) -> Result<bool, String> {
1493    let Ok(i) = key.parse::<usize>() else {
1494        return Ok(false);
1495    };
1496    let kind = kind_of(recv);
1497    // Coerced through the element type, so writing a Number into a 64-bit view
1498    // throws rather than storing an un-typed element.
1499    let n = coerce_val(&kind, val)?;
1500    if i >= view_len(recv) {
1501        return Ok(false);
1502    }
1503    let bpe = bytes_per_element(&kind);
1504    Ok(write_view_bytes(recv, i * bpe, &encode(&kind, &n)))
1505}
1506
1507/// Build a result of the same "species" as `recv`: a `Buffer` receiver yields a
1508/// `Buffer`, every other typed array yields its own kind. Node picks the result
1509/// type from the receiver's constructor, so `Buffer.from([1]).map(f)` is a
1510/// Buffer and `new Int32Array([1]).map(f)` is an `Int32Array`.
1511fn species(recv: &Value, kind: &str, elems: Vec<Value>) -> Value {
1512    if super::native_tag(recv).as_deref() == Some("Buffer") {
1513        let bytes: Vec<u8> = elems.iter().map(|x| num(x) as i64 as u8).collect();
1514        return super::buffer::from_bytes(&bytes);
1515    }
1516    make(kind, elems)
1517}
1518
1519/// Overwrite `recv`'s elements in place, for the methods that mutate and return
1520/// the receiver (`fill`, `reverse`, `sort`, `copyWithin`). Writes through to
1521/// whichever store backs it — the `ArrayBuffer` for a typed array, `@@bytes` for
1522/// a `Buffer`.
1523fn write_elems(recv: &Value, kind: &str, vals: &[Value]) -> Result<(), String> {
1524    if super::native_tag(recv).as_deref() == Some("TypedArray") {
1525        let bpe = bytes_per_element(kind);
1526        let coerced: Vec<Value> = vals
1527            .iter()
1528            .map(|v| coerce_val(kind, v))
1529            .collect::<Result<_, _>>()?;
1530        for (i, v) in coerced.iter().enumerate() {
1531            write_view_bytes(recv, i * bpe, &encode(kind, v));
1532        }
1533        return Ok(());
1534    }
1535    let field = "@@bytes";
1536    // Coerce OUTSIDE the host borrow: `coerce_val` re-enters the host to read a
1537    // BigInt and to allocate the wrapped one.
1538    let coerced: Vec<Value> = vals
1539        .iter()
1540        .map(|v| coerce_val(kind, v))
1541        .collect::<Result<_, _>>()?;
1542    with_host(|h| {
1543        if let Some(JsObj::Object(p)) = h.get(recv) {
1544            if let Some(arr) = p.get(field).cloned() {
1545                if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
1546                    for (i, v) in coerced.into_iter().enumerate() {
1547                        if i < items.len() {
1548                            items[i] = v;
1549                        }
1550                    }
1551                }
1552            }
1553        }
1554    });
1555    Ok(())
1556}
1557
1558/// Order `elems` the way `%TypedArray%.prototype.sort` (23.2.3.29) does, with
1559/// `cmp` as the optional user comparator. Shared with `toSorted` (23.2.3.33),
1560/// which is the same ordering over a copy.
1561fn sort_elements(elems: &mut Vec<Value>, kind: &str, cmp: Option<&Value>) -> Result<(), String> {
1562    let cmp = cmp.cloned().unwrap_or(Value::Undef);
1563    if with_host(|h| crate::host::is_callable(h, &cmp)) {
1564        // A user comparator goes through the same fallible merge sort
1565        // `Array.prototype.sort` uses: O(n log n) rather than the insertion sort
1566        // this was, and a comparator returning NaN keeps the pair's order
1567        // (23.2.4.1 step 3: NaN is +0) instead of swapping, which the `<= 0.0`
1568        // break got wrong.
1569        return crate::builtins::sort_values(elems, Some(&cmp));
1570    }
1571    // A typed array sorts NUMERICALLY by default, unlike `Array` which sorts by
1572    // string. Verified against node v26.7.0: `new Uint8Array([10,9,1]).sort()`
1573    // is `1,9,10` while `[10,9,1].sort()` is `1,10,9`.
1574    // A BigInt element cannot be ordered through an `f64` without collapsing
1575    // values more than 2^53 apart, so the 64-bit views compare the integers
1576    // themselves.
1577    if is_bigint_kind(kind) {
1578        let keys: Vec<num_bigint::BigInt> = elems.iter().map(bigint_of).collect();
1579        let mut idx: Vec<usize> = (0..elems.len()).collect();
1580        idx.sort_by(|a, b| keys[*a].cmp(&keys[*b]));
1581        *elems = idx.into_iter().map(|i| elems[i].clone()).collect();
1582    } else {
1583        elems.sort_by(|a, b| {
1584            num(a)
1585                .partial_cmp(&num(b))
1586                .unwrap_or(std::cmp::Ordering::Equal)
1587        });
1588    }
1589    Ok(())
1590}
1591
1592/// Resolve a relative index argument against `len` (negative counts from the
1593/// end), clamped into range — the `RelativeIndex` coercion the typed-array
1594/// methods share.
1595fn rel_index(args: &[Value], idx: usize, len: usize, default: usize) -> usize {
1596    if args.len() <= idx {
1597        return default;
1598    }
1599    let n = super::arg_num(args, idx);
1600    if n < 0.0 {
1601        (len as f64 + n).max(0.0) as usize
1602    } else {
1603        (n as usize).min(len)
1604    }
1605}
1606
1607/// Typed-array instance methods.
1608/// `toBase64` / `toHex` / `setFromBase64` / `setFromHex` — the `Uint8Array`
1609/// half of the base64/hex proposal. All four are brand-checked to `Uint8Array`:
1610/// every other view, and an ordinary array, is an incompatible receiver.
1611fn base64_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
1612    let kind = kind_of(recv);
1613    if kind != "Uint8Array" {
1614        // Node renders a non-view receiver by its brand and a WRONG view as
1615        // `undefined`, which reads oddly but is what it prints.
1616        // A typed array of the WRONG element kind renders as `undefined` here,
1617        // which reads oddly but is what node prints; every other receiver is
1618        // rendered the way the other brand checks render one, and never reaches
1619        // this arm — the dispatcher's guard catches it first.
1620        return Err(crate::host::type_error(&format!(
1621            "Method Uint8Array.prototype.{method} called on incompatible receiver undefined"
1622        )));
1623    }
1624    let bytes: Vec<u8> = elem_values(recv)
1625        .iter()
1626        .map(|v| with_host(|h| h.to_number(v)) as u8)
1627        .collect();
1628    match method {
1629        "toBase64" => {
1630            let (url, _) = base64_options(args.first())?;
1631            let omit = args
1632                .first()
1633                .filter(|v| !matches!(v, Value::Undef))
1634                .map(|o| {
1635                    with_host(|h| match h.get(o) {
1636                        Some(JsObj::Object(p)) => {
1637                            p.get("omitPadding").map(|v| h.truthy(v)).unwrap_or(false)
1638                        }
1639                        _ => false,
1640                    })
1641                })
1642                .unwrap_or(false);
1643            // The url alphabet only swaps the two characters — it does NOT drop
1644            // the padding, which `to_base64url` does for the `atob` callers.
1645            let mut s = super::to_base64(&bytes);
1646            if url {
1647                s = s.replace('+', "-").replace('/', "_");
1648            }
1649            if omit {
1650                s = s.trim_end_matches('=').to_string();
1651            }
1652            Ok(with_host(|h| h.new_str(s)))
1653        }
1654        "toHex" => Ok(with_host(|h| h.new_str(super::to_hex(&bytes)))),
1655        // `setFrom*` writes as much as FITS and reports how far it got, so a
1656        // short target is not an error — it stops at the last whole chunk.
1657        "setFromBase64" | "setFromHex" => {
1658            let s = base64_input(args)?;
1659            let (decoded, read) = if method == "setFromHex" {
1660                let d = decode_hex_strict(&s)?;
1661                let fits = d.len().min(bytes.len());
1662                (d[..fits].to_vec(), fits * 2)
1663            } else {
1664                let (url, last) = base64_options(args.get(1))?;
1665                // Decode only as much as the target can hold: whole 4-character
1666                // chunks, plus the final partial one when it still fits.
1667                let whole = (bytes.len() / 3) * 4;
1668                let head: String = s.chars().take(whole).collect();
1669                let (mut d, mut consumed) = decode_base64_strict(&head, url, last)?;
1670                if d.len() < bytes.len() {
1671                    let (full, full_read) = decode_base64_strict(&s, url, last)?;
1672                    if full.len() <= bytes.len() {
1673                        d = full;
1674                        consumed = full_read;
1675                    }
1676                }
1677                (d, consumed)
1678            };
1679            write_view_bytes(recv, 0, &decoded);
1680            Ok(with_host(|h| {
1681                let mut m = IndexMap::new();
1682                m.insert("read".to_string(), Value::Float(read as f64));
1683                m.insert("written".to_string(), Value::Float(decoded.len() as f64));
1684                h.new_object(m)
1685            }))
1686        }
1687        _ => unreachable!("caller gates the method name"),
1688    }
1689}
1690
1691pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
1692    // Every method over a DETACHED buffer throws, naming itself. An element
1693    // read and `length` answer zero instead, which is why this is a per-method
1694    // guard rather than a check inside the element accessors.
1695    if view_detached(recv) {
1696        return Err(detached_error("%TypedArray%.prototype", method, false));
1697    }
1698    if matches!(
1699        method,
1700        "toBase64" | "toHex" | "setFromBase64" | "setFromHex"
1701    ) {
1702        return base64_instance_call(recv, method, args);
1703    }
1704    let kind = kind_of(recv);
1705    // Elements travel as `Value`, not `f64`: a 64-bit view's are BigInts, and
1706    // rounding them through a double is exactly the loss those views exist to
1707    // avoid. The numeric kinds still hold `Value::Float`, so nothing about them
1708    // changes.
1709    let elems = elem_values(recv);
1710    // The callback-taking methods share one shape: invoke `cb(value, index,
1711    // receiver)` per element. They are inherited by `Buffer` too, which is why
1712    // they must live here rather than in either concrete type.
1713    // `forEach(fn, thisArg)` and its siblings bind `thisArg` as the callback's
1714    // `this`; it was being dropped, so `this` inside the callback was undefined.
1715    let this_arg = args.get(1).filter(|v| !matches!(v, Value::Undef)).cloned();
1716    let call_cb = |i: usize, v: &Value| -> Result<Value, String> {
1717        crate::host::invoke(
1718            &args.first().cloned().unwrap_or(Value::Undef),
1719            vec![v.clone(), Value::Float(i as f64), recv.clone()],
1720            this_arg.clone(),
1721        )
1722    };
1723    match method {
1724        "every" => {
1725            for (i, v) in elems.iter().enumerate() {
1726                let r = call_cb(i, v)?;
1727                if !with_host(|h| h.truthy(&r)) {
1728                    return Ok(Value::Bool(false));
1729                }
1730            }
1731            Ok(Value::Bool(true))
1732        }
1733        "some" => {
1734            for (i, v) in elems.iter().enumerate() {
1735                let r = call_cb(i, v)?;
1736                if with_host(|h| h.truthy(&r)) {
1737                    return Ok(Value::Bool(true));
1738                }
1739            }
1740            Ok(Value::Bool(false))
1741        }
1742        "forEach" => {
1743            for (i, v) in elems.iter().enumerate() {
1744                call_cb(i, v)?;
1745            }
1746            Ok(Value::Undef)
1747        }
1748        "map" => {
1749            let mut out = Vec::with_capacity(elems.len());
1750            for (i, v) in elems.iter().enumerate() {
1751                let r = call_cb(i, v)?;
1752                out.push(coerce_val(&kind, &r)?);
1753            }
1754            Ok(species(recv, &kind, out))
1755        }
1756        "filter" => {
1757            let mut out = Vec::new();
1758            for (i, v) in elems.iter().enumerate() {
1759                let r = call_cb(i, v)?;
1760                if with_host(|h| h.truthy(&r)) {
1761                    out.push(v.clone());
1762                }
1763            }
1764            Ok(species(recv, &kind, out))
1765        }
1766        "find" | "findIndex" | "findLast" | "findLastIndex" => {
1767            let last = method.starts_with("findLast");
1768            let idxs: Vec<usize> = if last {
1769                (0..elems.len()).rev().collect()
1770            } else {
1771                (0..elems.len()).collect()
1772            };
1773            for i in idxs {
1774                let r = call_cb(i, &elems[i])?;
1775                if with_host(|h| h.truthy(&r)) {
1776                    return Ok(if method.ends_with("Index") {
1777                        Value::Float(i as f64)
1778                    } else {
1779                        elems[i].clone()
1780                    });
1781                }
1782            }
1783            Ok(if method.ends_with("Index") {
1784                Value::Float(-1.0)
1785            } else {
1786                Value::Undef
1787            })
1788        }
1789        "reduce" | "reduceRight" => {
1790            let right = method == "reduceRight";
1791            let order: Vec<usize> = if right {
1792                (0..elems.len()).rev().collect()
1793            } else {
1794                (0..elems.len()).collect()
1795            };
1796            let cb = args.first().cloned().unwrap_or(Value::Undef);
1797            let mut it = order.into_iter();
1798            let mut acc = if args.len() >= 2 {
1799                args[1].clone()
1800            } else {
1801                match it.next() {
1802                    Some(i) => elems[i].clone(),
1803                    None => {
1804                        return Err(crate::host::type_error(
1805                            "Reduce of empty array with no initial value",
1806                        ))
1807                    }
1808                }
1809            };
1810            for i in it {
1811                acc = crate::host::invoke(
1812                    &cb,
1813                    vec![acc, elems[i].clone(), Value::Float(i as f64), recv.clone()],
1814                    None,
1815                )?;
1816            }
1817            Ok(acc)
1818        }
1819        "reverse" => {
1820            let mut out = elems.clone();
1821            out.reverse();
1822            write_elems(recv, &kind, &out)?;
1823            Ok(recv.clone())
1824        }
1825        "sort" => {
1826            let mut out = elems.clone();
1827            sort_elements(&mut out, &kind, args.first())?;
1828            write_elems(recv, &kind, &out)?;
1829            Ok(recv.clone())
1830        }
1831        "copyWithin" => {
1832            let len = elems.len();
1833            let target = rel_index(args, 0, len, 0);
1834            let start = rel_index(args, 1, len, 0);
1835            let end = rel_index(args, 2, len, len);
1836            let src: Vec<Value> = elems[start.min(end)..end.max(start)].to_vec();
1837            let mut out = elems.clone();
1838            for (k, v) in src.iter().enumerate() {
1839                if target + k < len {
1840                    out[target + k] = v.clone();
1841                }
1842            }
1843            write_elems(recv, &kind, &out)?;
1844            Ok(recv.clone())
1845        }
1846        "at" => {
1847            let n = super::arg_num(args, 0);
1848            let i = if n < 0.0 { elems.len() as f64 + n } else { n };
1849            if i < 0.0 || i >= elems.len() as f64 {
1850                return Ok(Value::Undef);
1851            }
1852            Ok(elems[i as usize].clone())
1853        }
1854        "lastIndexOf" => {
1855            let needle = args.first().cloned().unwrap_or(Value::Undef);
1856            let from = (args.len() > 1).then(|| super::arg_num(args, 1));
1857            let found = crate::builtins::search_start_last(from, elems.len()).and_then(|start| {
1858                elems[..=start]
1859                    .iter()
1860                    .rposition(|x| same_element(x, &needle, false))
1861            });
1862            Ok(Value::Float(found.map(|p| p as f64).unwrap_or(-1.0)))
1863        }
1864        // `%TypedArray%.prototype[Symbol.iterator]` IS `values` (23.2.3.35), so
1865        // it dispatches here rather than reporting itself missing:
1866        // `Uint8Array.prototype[Symbol.iterator].call(ta)` threw
1867        // `@@iterator is not a function`.
1868        "keys" | "values" | "entries" | "@@iterator" => {
1869            let items: Vec<Value> = with_host(|h| match method {
1870                "keys" => (0..elems.len()).map(|i| Value::Float(i as f64)).collect(),
1871                "values" | "@@iterator" => elems.clone(),
1872                _ => elems
1873                    .iter()
1874                    .enumerate()
1875                    .map(|(i, v)| h.new_array(vec![Value::Float(i as f64), v.clone()]))
1876                    .collect(),
1877            });
1878            Ok(with_host(|h| {
1879                h.alloc(JsObj::Iter {
1880                    items,
1881                    idx: 0,
1882                    array: None,
1883                })
1884            }))
1885        }
1886        "toString" | "join" => {
1887            let sep = if method == "join" && !args.is_empty() {
1888                super::arg_str(args, 0)
1889            } else {
1890                ",".into()
1891            };
1892            let parts: Vec<String> = with_host(|h| elems.iter().map(|n| h.str_of(n)).collect());
1893            Ok(with_host(|h| h.new_str(parts.join(&sep))))
1894        }
1895        "slice" | "subarray" => {
1896            let len = elems.len();
1897            let norm = |n: f64| -> usize {
1898                if n < 0.0 {
1899                    (len as f64 + n).max(0.0) as usize
1900                } else {
1901                    (n as usize).min(len)
1902                }
1903            };
1904            let s = if args.is_empty() {
1905                0
1906            } else {
1907                norm(super::arg_num(args, 0))
1908            };
1909            let e = if args.len() < 2 {
1910                len
1911            } else {
1912                norm(super::arg_num(args, 1))
1913            };
1914            let (lo, hi) = (s.min(e), e.max(s));
1915            // 23.2.3.30: `subarray` is a VIEW over the same buffer — writes
1916            // through it are seen by the original. `slice` copies (23.2.3.27).
1917            if method == "subarray" && super::native_tag(recv).as_deref() == Some("TypedArray") {
1918                if let Some((buf, off)) = view_base(recv) {
1919                    let bpe = bytes_per_element(&kind);
1920                    return Ok(make_view(&kind, &buf, off + lo * bpe, hi - lo));
1921                }
1922            }
1923            Ok(species(recv, &kind, elems[lo..hi].to_vec()))
1924        }
1925        "indexOf" => {
1926            let needle = args.first().cloned().unwrap_or(Value::Undef);
1927            let start = crate::builtins::search_start(super::arg_num(args, 1), elems.len());
1928            Ok(Value::Float(
1929                elems
1930                    .iter()
1931                    .skip(start)
1932                    .position(|x| same_element(x, &needle, false))
1933                    .map(|p| (p + start) as f64)
1934                    .unwrap_or(-1.0),
1935            ))
1936        }
1937        "includes" => {
1938            let needle = args.first().cloned().unwrap_or(Value::Undef);
1939            let start = crate::builtins::search_start(super::arg_num(args, 1), elems.len());
1940            Ok(Value::Bool(
1941                elems
1942                    .iter()
1943                    .skip(start)
1944                    .any(|x| same_element(x, &needle, true)),
1945            ))
1946        }
1947        // 23.2.3.9: `fill` writes THROUGH the view and answers the receiver. It
1948        // was building a fresh array instead, so the write was invisible —
1949        // `u.fill(9)` left `u` untouched, `u.fill(9) === u` was false, and a
1950        // second view onto the same `ArrayBuffer` saw none of it. The `start`
1951        // and `end` arguments were dropped too, so `fill(9, 1, 2)` overwrote the
1952        // whole array rather than one element.
1953        "fill" => {
1954            let len = elems.len();
1955            let v = coerce_val(&kind, args.first().unwrap_or(&Value::Undef))?;
1956            let start = rel_index(args, 1, len, 0);
1957            let end = rel_index(args, 2, len, len);
1958            let mut out = elems.clone();
1959            for slot in out.iter_mut().take(end).skip(start) {
1960                *slot = v.clone();
1961            }
1962            write_elems(recv, &kind, &out)?;
1963            Ok(recv.clone())
1964        }
1965        // The change-by-copy trio (23.2.3.32-34). Each answers a NEW view of the
1966        // receiver's own element kind — `TypedArrayCreateSameType`, not the
1967        // species path — so a `Buffer` receiver yields a `Uint8Array`, which is
1968        // what node reports.
1969        "toReversed" | "toSorted" => {
1970            let mut out = elems.clone();
1971            if method == "toReversed" {
1972                out.reverse();
1973            } else {
1974                sort_elements(&mut out, &kind, args.first())?;
1975            }
1976            Ok(make(&kind, out))
1977        }
1978        "with" => {
1979            let len = elems.len();
1980            let n = super::arg_num(args, 0);
1981            let i = if n < 0.0 { len as f64 + n } else { n };
1982            if !(0.0..len as f64).contains(&i) {
1983                return Err("RangeError: Invalid typed array index".into());
1984            }
1985            let mut out = elems.clone();
1986            out[i as usize] = coerce_val(&kind, args.get(1).unwrap_or(&Value::Undef))?;
1987            Ok(make(&kind, out))
1988        }
1989        "set" => {
1990            // `ta.set(src[, offset])` — write `src`'s values in place.
1991            let arg = args.first().cloned().unwrap_or(Value::Undef);
1992            let src = match super::native_tag(&arg).as_deref() {
1993                Some("TypedArray") | Some("Buffer") => elem_values(&arg),
1994                _ => crate::host::iter_all(&arg)
1995                    .unwrap_or_else(|_| crate::builtins::array_like_items(&arg)),
1996            };
1997            // 23.2.3.26: a negative offset, or a source that runs past the
1998            // end, is a RangeError. Neither may write a partial prefix.
1999            let off = super::arg_num(args, 1);
2000            let off = if off.is_nan() { 0.0 } else { off.trunc() };
2001            if off < 0.0 || off + src.len() as f64 > view_len(recv) as f64 {
2002                return Err(crate::host::range_error("offset is out of bounds"));
2003            }
2004            let off = off as usize;
2005            // Coerced outside the host borrow: a 64-bit element allocates.
2006            let src: Vec<Value> = src
2007                .iter()
2008                .map(|v| coerce_val(&kind, v))
2009                .collect::<Result<_, _>>()?;
2010            let bpe = bytes_per_element(&kind);
2011            let len = view_len(recv);
2012            for (k, v) in src.into_iter().enumerate() {
2013                if off + k < len {
2014                    write_view_bytes(recv, (off + k) * bpe, &encode(&kind, &v));
2015                }
2016            }
2017            Ok(Value::Undef)
2018        }
2019        _ => Err(crate::host::type_error(&format!(
2020            "{method} is not a function"
2021        ))),
2022    }
2023}
2024
2025// ── WeakRef (strong-ref approximation) ────────────────────────────────────────
2026
2027pub fn construct_weakref(args: &[Value]) -> Result<Value, String> {
2028    let target = args.first().cloned().unwrap_or(Value::Undef);
2029    Ok(with_host(|h| {
2030        let mut m = IndexMap::new();
2031        m.insert("@@native".into(), h.new_str("WeakRef"));
2032        m.insert("@@target".into(), target);
2033        h.new_object(m)
2034    }))
2035}
2036
2037pub fn weakref_call(recv: &Value, method: &str) -> Result<Value, String> {
2038    match method {
2039        "deref" => Ok(with_host(|h| match h.get(recv) {
2040            Some(JsObj::Object(p)) => p.get("@@target").cloned().unwrap_or(Value::Undef),
2041            _ => Value::Undef,
2042        })),
2043        _ => Err(crate::host::type_error(&format!(
2044            "{method} is not a function"
2045        ))),
2046    }
2047}
2048
2049// ── FinalizationRegistry (no-GC approximation) ────────────────────────────────
2050//
2051// This VM holds every value strongly (see `WeakRef` above), so a registered
2052// target is never reclaimed and the cleanup callback never fires. The ECMAScript
2053// spec permits an implementation to never call cleanup callbacks, so this is a
2054// conformant approximation: the constructor and `register`/`unregister` enforce
2055// their type checks and `unregister`'s bookkeeping exactly, only the (optional)
2056// callback invocation is absent. Registered unregister-tokens are tracked in a
2057// hidden `@@fr_tokens` array so `unregister` returns the correct boolean.
2058
2059/// Whether `v` is an Object (a valid `register` target / unregister token) — a
2060/// heap value that is not one of the primitive-wrapper heap variants.
2061fn is_object_value(v: &Value) -> bool {
2062    matches!(v, Value::Obj(_))
2063        && with_host(|h| {
2064            !matches!(
2065                h.get(v),
2066                Some(JsObj::Str(_))
2067                    | Some(JsObj::Symbol { .. })
2068                    | Some(JsObj::BigInt(_))
2069                    | Some(JsObj::Null)
2070            )
2071        })
2072}
2073
2074pub fn construct_finalization_registry(args: &[Value]) -> Result<Value, String> {
2075    let cb = args.first().cloned().unwrap_or(Value::Undef);
2076    if !with_host(|h| crate::host::is_callable(h, &cb)) {
2077        return Err(crate::host::type_error(
2078            "FinalizationRegistry: cleanup must be callable",
2079        ));
2080    }
2081    Ok(with_host(|h| {
2082        let tokens = h.new_array(Vec::new());
2083        let mut m = IndexMap::new();
2084        m.insert("@@native".into(), h.new_str("FinalizationRegistry"));
2085        m.insert("@@fr_cb".into(), cb);
2086        m.insert("@@fr_tokens".into(), tokens);
2087        h.new_object(m)
2088    }))
2089}
2090
2091pub fn finalization_registry_call(
2092    recv: &Value,
2093    method: &str,
2094    args: &[Value],
2095) -> Result<Value, String> {
2096    match method {
2097        "register" => {
2098            let target = args.first().cloned().unwrap_or(Value::Undef);
2099            let held = args.get(1).cloned().unwrap_or(Value::Undef);
2100            let token = args.get(2).cloned().unwrap_or(Value::Undef);
2101            if !is_object_value(&target) {
2102                // V8's wording is `invalid target`; the "must be an object"
2103                // phrasing was this file's own, not any engine's.
2104                return Err(crate::host::type_error(
2105                    "FinalizationRegistry.prototype.register: invalid target",
2106                ));
2107            }
2108            if with_host(|h| h.strict_eq(&target, &held)) {
2109                return Err(crate::host::type_error(
2110                    "FinalizationRegistry.prototype.register: target and holdings must not be same",
2111                ));
2112            }
2113            // A supplied unregister token must be an object; record it so a later
2114            // `unregister` can find (and drop) this registration.
2115            if !matches!(token, Value::Undef) {
2116                if !is_object_value(&token) {
2117                    return Err(crate::host::type_error(&format!(
2118                        "Invalid unregisterToken ('{}')",
2119                        with_host(|h| h.str_of(&token))
2120                    )));
2121                }
2122                with_host(|h| {
2123                    let toks = registry_tokens(h, recv);
2124                    if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
2125                        items.push(token);
2126                    }
2127                });
2128            }
2129            Ok(Value::Undef)
2130        }
2131        "unregister" => {
2132            let token = args.first().cloned().unwrap_or(Value::Undef);
2133            if !is_object_value(&token) {
2134                // V8 names the token and does not mention the method.
2135                return Err(crate::host::type_error(&format!(
2136                    "Invalid unregisterToken ('{}')",
2137                    with_host(|h| h.str_of(&token))
2138                )));
2139            }
2140            Ok(Value::Bool(with_host(|h| {
2141                let toks = registry_tokens(h, recv);
2142                let kept: Vec<Value> = match h.get(&toks) {
2143                    Some(JsObj::Array(items)) => items
2144                        .iter()
2145                        .filter(|t| !h.strict_eq(t, &token))
2146                        .cloned()
2147                        .collect(),
2148                    _ => Vec::new(),
2149                };
2150                let removed = match h.get(&toks) {
2151                    Some(JsObj::Array(items)) => items.len() != kept.len(),
2152                    _ => false,
2153                };
2154                if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
2155                    *items = kept;
2156                }
2157                removed
2158            })))
2159        }
2160        _ => Err(crate::host::type_error(&format!(
2161            "{method} is not a function"
2162        ))),
2163    }
2164}
2165
2166/// The hidden `@@fr_tokens` array backing a `FinalizationRegistry`.
2167fn registry_tokens(h: &crate::host::JsHost, recv: &Value) -> Value {
2168    match h.get(recv) {
2169        Some(JsObj::Object(p)) => p.get("@@fr_tokens").cloned().unwrap_or(Value::Undef),
2170        _ => Value::Undef,
2171    }
2172}
2173
2174// ── TextEncoder / TextDecoder ─────────────────────────────────────────────────
2175
2176pub fn construct_text_encoder() -> Result<Value, String> {
2177    Ok(with_host(|h| {
2178        let mut m = IndexMap::new();
2179        m.insert("@@native".into(), h.new_str("TextEncoder"));
2180        // `encoding` is a getter on the prototype, so the value lives in the
2181        // hidden slot the getter reads. As an own property it enumerated —
2182        // `Object.keys(new TextEncoder())` answered `["encoding"]` where node
2183        // answers `[]`, and `JSON.stringify` of anything holding one carried it.
2184        m.insert("@@encoding".into(), h.new_str("utf-8"));
2185        h.new_object(m)
2186    }))
2187}
2188
2189pub fn text_encoder_call(_recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
2190    match method {
2191        // `encode(str)` → a Uint8Array of the UTF-8 bytes.
2192        "encode" => {
2193            let s = super::arg_str(args, 0);
2194            Ok(make(
2195                "Uint8Array",
2196                s.as_bytes()
2197                    .iter()
2198                    .map(|b| Value::Float(*b as f64))
2199                    .collect(),
2200            ))
2201        }
2202        _ => Err(crate::host::type_error(&format!(
2203            "{method} is not a function"
2204        ))),
2205    }
2206}
2207
2208/// The WHATWG encoding a label names, as node reports it through
2209/// `decoder.encoding`. The label is NOT the encoding: `latin1`, `ascii` and
2210/// `iso-8859-1` all name `windows-1252`, and `ucs-2` names `utf-16le`. This
2211/// echoed the label back, so `new TextDecoder("latin1").encoding` read
2212/// `"latin1"` — a name node never reports — and an unknown label was accepted
2213/// and then decoded as UTF-8.
2214fn encoding_for_label(label: &str) -> Option<&'static str> {
2215    Some(match label.trim().to_ascii_lowercase().as_str() {
2216        "utf-8" | "utf8" | "unicode-1-1-utf-8" | "unicode11utf8" | "unicode20utf8"
2217        | "x-unicode20utf8" => "utf-8",
2218        "latin1" | "iso-8859-1" | "iso8859-1" | "iso88591" | "ascii" | "us-ascii" | "cp1252"
2219        | "cp819" | "ibm819" | "l1" | "windows-1252" | "x-cp1252" => "windows-1252",
2220        "utf-16le" | "utf-16" | "ucs-2" | "ucs2" | "unicodefeff" | "unicodefffe"
2221        | "iso-10646-ucs-2" | "csunicode" => "utf-16le",
2222        _ => return None,
2223    })
2224}
2225
2226pub fn construct_text_decoder(args: &[Value]) -> Result<Value, String> {
2227    let label = if args.is_empty() || matches!(args[0], Value::Undef) {
2228        "utf-8".to_string()
2229    } else {
2230        super::arg_str(args, 0)
2231    };
2232    let Some(encoding) = encoding_for_label(&label) else {
2233        return Err(crate::host::coded_error(
2234            "RangeError",
2235            "ERR_ENCODING_NOT_SUPPORTED",
2236            &format!("The \"{label}\" encoding is not supported"),
2237        ));
2238    };
2239    // `fatal` and `ignoreBOM` were not read at all, so a decoder asked to reject
2240    // malformed input accepted it and one asked to keep the BOM never saw one —
2241    // both options silently did nothing.
2242    let flag = |key: &str| {
2243        args.get(1)
2244            .map(|o| crate::builtins::get_property(o, key).unwrap_or(Value::Undef))
2245            .map(|v| with_host(|h| h.truthy(&v)))
2246            .unwrap_or(false)
2247    };
2248    let (fatal, ignore_bom) = (flag("fatal"), flag("ignoreBOM"));
2249    Ok(with_host(|h| {
2250        let mut m = IndexMap::new();
2251        m.insert("@@native".into(), h.new_str("TextDecoder"));
2252        m.insert("@@encoding".into(), h.new_str(encoding.to_string()));
2253        m.insert("@@fatal".into(), Value::Bool(fatal));
2254        m.insert("@@ignoreBOM".into(), Value::Bool(ignore_bom));
2255        h.new_object(m)
2256    }))
2257}
2258
2259/// windows-1252's 0x80..=0x9F range, which is NOT latin1's: those 32 positions
2260/// carry the typographic characters (curly quotes, the euro sign, the dashes)
2261/// rather than C1 control codes. Decoding them as latin1 — `b as char`, what
2262/// this did — turned every smart quote in a Windows-encoded file into a control
2263/// character.
2264const CP1252_HIGH: [char; 32] = [
2265    '\u{20ac}', '\u{81}', '\u{201a}', '\u{192}', '\u{201e}', '\u{2026}', '\u{2020}', '\u{2021}',
2266    '\u{2c6}', '\u{2030}', '\u{160}', '\u{2039}', '\u{152}', '\u{8d}', '\u{17d}', '\u{8f}',
2267    '\u{90}', '\u{2018}', '\u{2019}', '\u{201c}', '\u{201d}', '\u{2022}', '\u{2013}', '\u{2014}',
2268    '\u{2dc}', '\u{2122}', '\u{161}', '\u{203a}', '\u{153}', '\u{9d}', '\u{17e}', '\u{178}',
2269];
2270
2271pub fn text_decoder_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
2272    match method {
2273        // `decode(bytes)` → a string from the buffer's UTF-8 (or latin1) bytes.
2274        "decode" => {
2275            let bytes: Vec<u8> = elems_of(&args.first().cloned().unwrap_or(Value::Undef))
2276                .unwrap_or_default()
2277                .iter()
2278                .map(|n| *n as u8)
2279                .collect();
2280            let slot = |key: &str| {
2281                with_host(|h| match h.get(recv) {
2282                    Some(JsObj::Object(p)) => p.get(key).cloned(),
2283                    _ => None,
2284                })
2285            };
2286            let enc = slot("@@encoding")
2287                .map(|v| with_host(|h| h.str_of(&v)))
2288                .unwrap_or_else(|| "utf-8".into());
2289            let flag = |key: &str| matches!(slot(key), Some(Value::Bool(true)));
2290            let s = match enc.as_str() {
2291                "windows-1252" => bytes
2292                    .iter()
2293                    .map(|b| match b {
2294                        0x80..=0x9f => CP1252_HIGH[(b - 0x80) as usize],
2295                        _ => *b as char,
2296                    })
2297                    .collect(),
2298                "utf-16le" => {
2299                    let units: Vec<u16> = bytes
2300                        .chunks_exact(2)
2301                        .map(|c| u16::from_le_bytes([c[0], c[1]]))
2302                        .collect();
2303                    String::from_utf16_lossy(&units)
2304                }
2305                // A `fatal` decoder REJECTS malformed input rather than
2306                // substituting U+FFFD. Both behaved as the lossy form, so bytes
2307                // that are not valid UTF-8 came back as replacement characters
2308                // from a decoder built to refuse them.
2309                _ if flag("@@fatal") => match std::str::from_utf8(&bytes) {
2310                    Ok(s) => s.to_string(),
2311                    Err(_) => {
2312                        return Err(crate::host::coded_error(
2313                            "TypeError",
2314                            "ERR_ENCODING_INVALID_ENCODED_DATA",
2315                            &format!("The encoded data was not valid for encoding {enc}"),
2316                        ))
2317                    }
2318                },
2319                _ => String::from_utf8_lossy(&bytes).into_owned(),
2320            };
2321            // A leading BOM is REMOVED unless `ignoreBOM` asked to keep it.
2322            let s = match s.strip_prefix('\u{feff}') {
2323                Some(rest) if !flag("@@ignoreBOM") => rest.to_string(),
2324                _ => s,
2325            };
2326            Ok(with_host(|h| h.new_str(s)))
2327        }
2328        _ => Err(crate::host::type_error(&format!(
2329            "{method} is not a function"
2330        ))),
2331    }
2332}