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