Skip to main content

nodejs/stdlib/
typedarray.rs

1//! JavaScript typed arrays (`Uint8Array`/`Int8Array`/…/`Float64Array`),
2//! `ArrayBuffer`, `WeakRef`, and `TextEncoder`/`TextDecoder`.
3//!
4//! A typed array is a plain object tagged `@@native = "TypedArray"` carrying its
5//! kind (`@@kind`), its elements as a hidden `@@elems` array of numbers, and the
6//! enumerable `length`/`byteLength`/`BYTES_PER_ELEMENT` data properties JS code
7//! reads directly. Element indexing (`ta[i]` get/set) is special-cased in
8//! `builtins::get_property`/`set_property` via `elem_get`/`elem_set` here, which
9//! also apply each kind's coercion (integer wrap / clamp / float).
10//!
11//! `WeakRef` holds a *strong* reference (`deref()` always returns the target) —
12//! node-js has no GC of JS objects, so this is observably correct for the
13//! express dependency tree (object-inspect/qs/side-channel only ever `deref()`).
14
15use crate::host::{with_host, JsObj};
16use fusevm::Value;
17use indexmap::IndexMap;
18
19pub const STATIC_METHODS: &[&str] = &["from", "of"];
20
21/// The nine element kinds plus `ArrayBuffer` (which carries only a byte length).
22pub fn is_ctor(name: &str) -> bool {
23    matches!(
24        name,
25        "Uint8Array"
26            | "Int8Array"
27            | "Uint8ClampedArray"
28            | "Int16Array"
29            | "Uint16Array"
30            | "Int32Array"
31            | "Uint32Array"
32            | "Float32Array"
33            | "Float64Array"
34            | "ArrayBuffer"
35    )
36}
37
38/// Bytes per element for a typed-array kind.
39fn bytes_per_element(kind: &str) -> usize {
40    match kind {
41        "Int8Array" | "Uint8Array" | "Uint8ClampedArray" => 1,
42        "Int16Array" | "Uint16Array" => 2,
43        "Int32Array" | "Uint32Array" | "Float32Array" => 4,
44        "Float64Array" => 8,
45        _ => 1,
46    }
47}
48
49/// Coerce a JS number into the value stored for `kind` (integer wrap, unsigned
50/// clamp, or float), mirroring the `ToInt8`/`ToUint8Clamp`/… abstract ops.
51fn coerce(kind: &str, n: f64) -> f64 {
52    match kind {
53        "Int8Array" => (n as i64 as i8) as f64,
54        "Uint8Array" => (n as i64 as u8) as f64,
55        "Uint8ClampedArray" => {
56            if n.is_nan() {
57                0.0
58            } else {
59                n.round().clamp(0.0, 255.0)
60            }
61        }
62        "Int16Array" => (n as i64 as i16) as f64,
63        "Uint16Array" => (n as i64 as u16) as f64,
64        "Int32Array" => (n as i64 as i32) as f64,
65        "Uint32Array" => (n as i64 as u32) as f64,
66        "Float32Array" => n as f32 as f64,
67        _ => n, // Float64Array
68    }
69}
70
71/// Build a typed array of `kind` from already-coerced element values.
72fn make(kind: &str, elems: Vec<f64>) -> Value {
73    with_host(|h| {
74        let bpe = bytes_per_element(kind);
75        let len = elems.len();
76        let arr = h.new_array(elems.into_iter().map(Value::Float).collect());
77        let mut m = IndexMap::new();
78        m.insert("@@native".into(), h.new_str("TypedArray"));
79        m.insert("@@kind".into(), h.new_str(kind));
80        m.insert("@@elems".into(), arr);
81        m.insert("length".into(), Value::Float(len as f64));
82        m.insert("byteLength".into(), Value::Float((len * bpe) as f64));
83        m.insert("BYTES_PER_ELEMENT".into(), Value::Float(bpe as f64));
84        h.new_object(m)
85    })
86}
87
88/// `new Uint8Array(...)` etc. `ArrayBuffer` is a byte container with only a
89/// `byteLength`.
90pub fn construct(kind: &str, args: &[Value]) -> Result<Value, String> {
91    if kind == "ArrayBuffer" {
92        let n = super::arg_num(args, 0).max(0.0) as usize;
93        return Ok(with_host(|h| {
94            let mut m = IndexMap::new();
95            m.insert("@@native".into(), h.new_str("ArrayBuffer"));
96            m.insert("byteLength".into(), Value::Float(n as f64));
97            h.new_object(m)
98        }));
99    }
100    let elems = build_elems(kind, args)?;
101    Ok(make(kind, elems))
102}
103
104/// Element vector for a typed-array construction from its first argument:
105/// a number → that many zeroed slots; an array/iterable/typed-array → its coerced
106/// values; otherwise → empty.
107fn build_elems(kind: &str, args: &[Value]) -> Result<Vec<f64>, String> {
108    match args.first() {
109        None | Some(Value::Undef) => Ok(Vec::new()),
110        Some(Value::Int(_)) | Some(Value::Float(_)) => {
111            let n = super::arg_num(args, 0).max(0.0) as usize;
112            Ok(vec![0.0; n])
113        }
114        Some(v) => {
115            // Another typed array / Buffer → copy its elements.
116            if let Some(src) = elems_of(v) {
117                return Ok(src.iter().map(|x| coerce(kind, *x)).collect());
118            }
119            // A plain array or arraylike → coerce each entry.
120            let items = crate::host::iter_all(v).unwrap_or_default();
121            Ok(items
122                .iter()
123                .map(|x| coerce(kind, with_host(|h| h.to_number(x))))
124                .collect())
125        }
126    }
127}
128
129/// `Uint8Array.from(iterable[, mapFn])` / `Uint8Array.of(...items)`.
130pub fn static_call(kind: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
131    Some(match method {
132        "of" => Ok(make(
133            kind,
134            args.iter()
135                .map(|x| coerce(kind, with_host(|h| h.to_number(x))))
136                .collect(),
137        )),
138        "from" => from(kind, args),
139        _ => return None,
140    })
141}
142
143fn from(kind: &str, args: &[Value]) -> Result<Value, String> {
144    let src = args.first().cloned().unwrap_or(Value::Undef);
145    let map_fn = args
146        .get(1)
147        .cloned()
148        .filter(|f| with_host(|h| crate::host::is_callable(h, f)));
149    let items = if let Some(e) = elems_of(&src) {
150        e.into_iter().map(Value::Float).collect()
151    } else {
152        crate::host::iter_all(&src).unwrap_or_default()
153    };
154    let mut out = Vec::with_capacity(items.len());
155    for (i, it) in items.into_iter().enumerate() {
156        let mapped = match &map_fn {
157            Some(f) => crate::host::invoke(f, vec![it, Value::Float(i as f64)], None)?,
158            None => it,
159        };
160        out.push(coerce(kind, with_host(|h| h.to_number(&mapped))));
161    }
162    Ok(make(kind, out))
163}
164
165/// The element values of a typed array / Buffer (`None` for anything else).
166fn elems_of(v: &Value) -> Option<Vec<f64>> {
167    let tag = super::native_tag(v)?;
168    let field = match tag.as_str() {
169        "TypedArray" => "@@elems",
170        "Buffer" => "@@bytes",
171        _ => return None,
172    };
173    with_host(|h| match h.get(v) {
174        Some(JsObj::Object(p)) => match p.get(field).and_then(|a| h.get(a)) {
175            Some(JsObj::Array(items)) => Some(items.iter().map(|x| h.to_number(x)).collect()),
176            _ => None,
177        },
178        _ => None,
179    })
180}
181
182/// The `@@kind` of a typed-array receiver (defaults to `Uint8Array`).
183fn kind_of(recv: &Value) -> String {
184    with_host(|h| match h.get(recv) {
185        Some(JsObj::Object(p)) => p
186            .get("@@kind")
187            .map(|v| h.str_of(v))
188            .unwrap_or_else(|| "Uint8Array".into()),
189        _ => "Uint8Array".into(),
190    })
191}
192
193// ── element indexing (called from builtins::get_property/set_property) ────────
194
195/// `ta[i]` read: the element at char/index `i`, or `None` if `i` is out of range
196/// or not an integer index.
197pub fn elem_get(recv: &Value, key: &str) -> Option<Value> {
198    let i: usize = key.parse().ok()?;
199    with_host(|h| match h.get(recv) {
200        Some(JsObj::Object(p)) => match p.get("@@elems").and_then(|a| h.get(a)) {
201            Some(JsObj::Array(items)) => items.get(i).cloned(),
202            _ => None,
203        },
204        _ => None,
205    })
206}
207
208/// `ta[i] = v` write (coerced to the kind). Returns true if `i` is a valid index.
209pub fn elem_set(recv: &Value, key: &str, val: &Value) -> bool {
210    let Ok(i) = key.parse::<usize>() else {
211        return false;
212    };
213    let kind = kind_of(recv);
214    let n = coerce(&kind, with_host(|h| h.to_number(val)));
215    with_host(|h| {
216        if let Some(JsObj::Object(p)) = h.get(recv) {
217            if let Some(arr) = p.get("@@elems").cloned() {
218                if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
219                    if i < items.len() {
220                        items[i] = Value::Float(n);
221                        return true;
222                    }
223                }
224            }
225        }
226        false
227    })
228}
229
230/// Typed-array instance methods.
231pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
232    let kind = kind_of(recv);
233    let elems = elems_of(recv).unwrap_or_default();
234    match method {
235        "toString" | "join" => {
236            let sep = if method == "join" && !args.is_empty() {
237                super::arg_str(args, 0)
238            } else {
239                ",".into()
240            };
241            let parts: Vec<String> =
242                with_host(|h| elems.iter().map(|n| h.str_of(&Value::Float(*n))).collect());
243            Ok(with_host(|h| h.new_str(parts.join(&sep))))
244        }
245        "slice" | "subarray" => {
246            let len = elems.len();
247            let norm = |n: f64| -> usize {
248                if n < 0.0 {
249                    (len as f64 + n).max(0.0) as usize
250                } else {
251                    (n as usize).min(len)
252                }
253            };
254            let s = if args.is_empty() {
255                0
256            } else {
257                norm(super::arg_num(args, 0))
258            };
259            let e = if args.len() < 2 {
260                len
261            } else {
262                norm(super::arg_num(args, 1))
263            };
264            Ok(make(&kind, elems[s.min(e)..e.max(s)].to_vec()))
265        }
266        "indexOf" => {
267            let needle = super::arg_num(args, 0);
268            Ok(Value::Float(
269                elems
270                    .iter()
271                    .position(|x| *x == needle)
272                    .map(|p| p as f64)
273                    .unwrap_or(-1.0),
274            ))
275        }
276        "includes" => {
277            let needle = super::arg_num(args, 0);
278            Ok(Value::Bool(elems.contains(&needle)))
279        }
280        "fill" => {
281            let v = coerce(&kind, super::arg_num(args, 0));
282            Ok(make(&kind, vec![v; elems.len()]))
283        }
284        "set" => {
285            // `ta.set(src[, offset])` — write `src`'s values in place.
286            let src = elems_of(&args.first().cloned().unwrap_or(Value::Undef))
287                .or_else(|| {
288                    Some(
289                        crate::host::iter_all(&args.first().cloned().unwrap_or(Value::Undef))
290                            .ok()?
291                            .iter()
292                            .map(|x| with_host(|h| h.to_number(x)))
293                            .collect(),
294                    )
295                })
296                .unwrap_or_default();
297            let off = super::arg_num(args, 1).max(0.0) as usize;
298            with_host(|h| {
299                if let Some(JsObj::Object(p)) = h.get(recv) {
300                    if let Some(arr) = p.get("@@elems").cloned() {
301                        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
302                            for (k, v) in src.iter().enumerate() {
303                                if off + k < items.len() {
304                                    items[off + k] = Value::Float(coerce(&kind, *v));
305                                }
306                            }
307                        }
308                    }
309                }
310            });
311            Ok(Value::Undef)
312        }
313        _ => Err(crate::host::type_error(&format!(
314            "{method} is not a function"
315        ))),
316    }
317}
318
319// ── WeakRef (strong-ref approximation) ────────────────────────────────────────
320
321pub fn construct_weakref(args: &[Value]) -> Result<Value, String> {
322    let target = args.first().cloned().unwrap_or(Value::Undef);
323    Ok(with_host(|h| {
324        let mut m = IndexMap::new();
325        m.insert("@@native".into(), h.new_str("WeakRef"));
326        m.insert("@@target".into(), target);
327        h.new_object(m)
328    }))
329}
330
331pub fn weakref_call(recv: &Value, method: &str) -> Result<Value, String> {
332    match method {
333        "deref" => Ok(with_host(|h| match h.get(recv) {
334            Some(JsObj::Object(p)) => p.get("@@target").cloned().unwrap_or(Value::Undef),
335            _ => Value::Undef,
336        })),
337        _ => Err(crate::host::type_error(&format!(
338            "{method} is not a function"
339        ))),
340    }
341}
342
343// ── FinalizationRegistry (no-GC approximation) ────────────────────────────────
344//
345// This VM holds every value strongly (see `WeakRef` above), so a registered
346// target is never reclaimed and the cleanup callback never fires. The ECMAScript
347// spec permits an implementation to never call cleanup callbacks, so this is a
348// conformant approximation: the constructor and `register`/`unregister` enforce
349// their type checks and `unregister`'s bookkeeping exactly, only the (optional)
350// callback invocation is absent. Registered unregister-tokens are tracked in a
351// hidden `@@fr_tokens` array so `unregister` returns the correct boolean.
352
353/// Whether `v` is an Object (a valid `register` target / unregister token) — a
354/// heap value that is not one of the primitive-wrapper heap variants.
355fn is_object_value(v: &Value) -> bool {
356    matches!(v, Value::Obj(_))
357        && with_host(|h| {
358            !matches!(
359                h.get(v),
360                Some(JsObj::Str(_))
361                    | Some(JsObj::Symbol { .. })
362                    | Some(JsObj::BigInt(_))
363                    | Some(JsObj::Null)
364            )
365        })
366}
367
368pub fn construct_finalization_registry(args: &[Value]) -> Result<Value, String> {
369    let cb = args.first().cloned().unwrap_or(Value::Undef);
370    if !with_host(|h| crate::host::is_callable(h, &cb)) {
371        return Err(crate::host::type_error(
372            "FinalizationRegistry: cleanup must be callable",
373        ));
374    }
375    Ok(with_host(|h| {
376        let tokens = h.new_array(Vec::new());
377        let mut m = IndexMap::new();
378        m.insert("@@native".into(), h.new_str("FinalizationRegistry"));
379        m.insert("@@fr_cb".into(), cb);
380        m.insert("@@fr_tokens".into(), tokens);
381        h.new_object(m)
382    }))
383}
384
385pub fn finalization_registry_call(
386    recv: &Value,
387    method: &str,
388    args: &[Value],
389) -> Result<Value, String> {
390    match method {
391        "register" => {
392            let target = args.first().cloned().unwrap_or(Value::Undef);
393            let held = args.get(1).cloned().unwrap_or(Value::Undef);
394            let token = args.get(2).cloned().unwrap_or(Value::Undef);
395            if !is_object_value(&target) {
396                return Err(crate::host::type_error(
397                    "FinalizationRegistry.prototype.register: target must be an object",
398                ));
399            }
400            if with_host(|h| h.strict_eq(&target, &held)) {
401                return Err(crate::host::type_error(
402                    "FinalizationRegistry.prototype.register: target and holdings must not be same",
403                ));
404            }
405            // A supplied unregister token must be an object; record it so a later
406            // `unregister` can find (and drop) this registration.
407            if !matches!(token, Value::Undef) {
408                if !is_object_value(&token) {
409                    return Err(crate::host::type_error(
410                        "FinalizationRegistry.prototype.register: unregister token must be an object",
411                    ));
412                }
413                with_host(|h| {
414                    let toks = registry_tokens(h, recv);
415                    if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
416                        items.push(token);
417                    }
418                });
419            }
420            Ok(Value::Undef)
421        }
422        "unregister" => {
423            let token = args.first().cloned().unwrap_or(Value::Undef);
424            if !is_object_value(&token) {
425                return Err(crate::host::type_error(
426                    "FinalizationRegistry.prototype.unregister: unregister token must be an object",
427                ));
428            }
429            Ok(Value::Bool(with_host(|h| {
430                let toks = registry_tokens(h, recv);
431                let kept: Vec<Value> = match h.get(&toks) {
432                    Some(JsObj::Array(items)) => items
433                        .iter()
434                        .filter(|t| !h.strict_eq(t, &token))
435                        .cloned()
436                        .collect(),
437                    _ => Vec::new(),
438                };
439                let removed = match h.get(&toks) {
440                    Some(JsObj::Array(items)) => items.len() != kept.len(),
441                    _ => false,
442                };
443                if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
444                    *items = kept;
445                }
446                removed
447            })))
448        }
449        _ => Err(crate::host::type_error(&format!(
450            "{method} is not a function"
451        ))),
452    }
453}
454
455/// The hidden `@@fr_tokens` array backing a `FinalizationRegistry`.
456fn registry_tokens(h: &crate::host::JsHost, recv: &Value) -> Value {
457    match h.get(recv) {
458        Some(JsObj::Object(p)) => p.get("@@fr_tokens").cloned().unwrap_or(Value::Undef),
459        _ => Value::Undef,
460    }
461}
462
463// ── TextEncoder / TextDecoder ─────────────────────────────────────────────────
464
465pub fn construct_text_encoder() -> Result<Value, String> {
466    Ok(with_host(|h| {
467        let mut m = IndexMap::new();
468        m.insert("@@native".into(), h.new_str("TextEncoder"));
469        m.insert("encoding".into(), h.new_str("utf-8"));
470        h.new_object(m)
471    }))
472}
473
474pub fn text_encoder_call(_recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
475    match method {
476        // `encode(str)` → a Uint8Array of the UTF-8 bytes.
477        "encode" => {
478            let s = super::arg_str(args, 0);
479            Ok(make(
480                "Uint8Array",
481                s.as_bytes().iter().map(|b| *b as f64).collect(),
482            ))
483        }
484        _ => Err(crate::host::type_error(&format!(
485            "{method} is not a function"
486        ))),
487    }
488}
489
490pub fn construct_text_decoder(args: &[Value]) -> Result<Value, String> {
491    let label = if args.is_empty() {
492        "utf-8".to_string()
493    } else {
494        super::arg_str(args, 0)
495    };
496    Ok(with_host(|h| {
497        let mut m = IndexMap::new();
498        m.insert("@@native".into(), h.new_str("TextDecoder"));
499        m.insert("encoding".into(), h.new_str(label.to_ascii_lowercase()));
500        h.new_object(m)
501    }))
502}
503
504pub fn text_decoder_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
505    match method {
506        // `decode(bytes)` → a string from the buffer's UTF-8 (or latin1) bytes.
507        "decode" => {
508            let bytes: Vec<u8> = elems_of(&args.first().cloned().unwrap_or(Value::Undef))
509                .unwrap_or_default()
510                .iter()
511                .map(|n| *n as u8)
512                .collect();
513            let enc = with_host(|h| match h.get(recv) {
514                Some(JsObj::Object(p)) => p
515                    .get("encoding")
516                    .map(|v| h.str_of(v))
517                    .unwrap_or_else(|| "utf-8".into()),
518                _ => "utf-8".into(),
519            });
520            let s = match enc.as_str() {
521                "latin1" | "iso-8859-1" | "ascii" => bytes.iter().map(|b| *b as char).collect(),
522                _ => String::from_utf8_lossy(&bytes).into_owned(),
523            };
524            Ok(with_host(|h| h.new_str(s)))
525        }
526        _ => Err(crate::host::type_error(&format!(
527            "{method} is not a function"
528        ))),
529    }
530}