Skip to main content

oxide_core/vm/
error.rs

1//! Error objects the loop and the natives throw: an ordinary object flagged
2//! `ERROR`, with the kind's prototype and an own `message`; `name` lives on the
3//! prototype. Each carries QuickJS's `stack` backtrace.
4
5use std::fmt::Write;
6
7use super::{Cx, Throw, frame_flags};
8use crate::heap::HeapError;
9use crate::runtime::{Intrinsic, Names, Realm};
10use crate::shape::PropertyFlags;
11use crate::value::{ObjectId, Value};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ErrorKind {
15    Error,
16    Eval,
17    Range,
18    Reference,
19    Syntax,
20    Type,
21    Internal,
22    Uri,
23    Aggregate,
24}
25
26impl ErrorKind {
27    pub const ALL: [ErrorKind; 9] = [
28        ErrorKind::Error,
29        ErrorKind::Eval,
30        ErrorKind::Range,
31        ErrorKind::Reference,
32        ErrorKind::Syntax,
33        ErrorKind::Type,
34        ErrorKind::Internal,
35        ErrorKind::Uri,
36        ErrorKind::Aggregate,
37    ];
38
39    pub const fn name(self) -> &'static str {
40        match self {
41            ErrorKind::Error => "Error",
42            ErrorKind::Eval => "EvalError",
43            ErrorKind::Range => "RangeError",
44            ErrorKind::Reference => "ReferenceError",
45            ErrorKind::Syntax => "SyntaxError",
46            ErrorKind::Type => "TypeError",
47            ErrorKind::Internal => "InternalError",
48            ErrorKind::Uri => "URIError",
49            ErrorKind::Aggregate => "AggregateError",
50        }
51    }
52
53    pub const fn prototype(self) -> Intrinsic {
54        match self {
55            ErrorKind::Error => Intrinsic::ErrorPrototype,
56            ErrorKind::Eval => Intrinsic::EvalErrorPrototype,
57            ErrorKind::Range => Intrinsic::RangeErrorPrototype,
58            ErrorKind::Reference => Intrinsic::ReferenceErrorPrototype,
59            ErrorKind::Syntax => Intrinsic::SyntaxErrorPrototype,
60            ErrorKind::Type => Intrinsic::TypeErrorPrototype,
61            ErrorKind::Internal => Intrinsic::InternalErrorPrototype,
62            ErrorKind::Uri => Intrinsic::UriErrorPrototype,
63            ErrorKind::Aggregate => Intrinsic::AggregateErrorPrototype,
64        }
65    }
66}
67
68/// Where in which source an error is: a syntax error's file, line and
69/// column (one-based).
70#[derive(Clone, Copy, Debug)]
71pub(crate) struct Location<'a> {
72    pub file: &'a str,
73    pub line: u32,
74    pub column: u32,
75}
76
77/// A new error object of `kind`; `message` must be rooted (or not a heap
78/// value) by the caller.
79pub(crate) fn new_error(
80    cx: &mut Cx<'_>,
81    kind: ErrorKind,
82    message: Value,
83) -> Result<Value, HeapError> {
84    new_error_at(cx, kind, message, None, 0)
85}
86
87/// [`new_error`] located at `location` (its `fileName`, `lineNumber` and
88/// `columnNumber`, and the first line of its `stack`), with the running
89/// frames' backtrace after it but the `omit` innermost ([`add_stack_at`]).
90pub(crate) fn new_error_at(
91    cx: &mut Cx<'_>,
92    kind: ErrorKind,
93    message: Value,
94    location: Option<Location<'_>>,
95    omit: usize,
96) -> Result<Value, HeapError> {
97    let prototype = cx.realm.intrinsic(kind.prototype());
98    let heap = &mut *cx.heap;
99    let mark = heap.roots().mark();
100    heap.roots().push(message);
101    let result = (|| {
102        let object = heap.alloc_object(crate::ObjectKind::Ordinary, Some(prototype), 1)?;
103        heap.set_object_flag(object, crate::heap::object::flags::ERROR);
104        if !message.is_undefined() {
105            let key = Names::key(cx.realm.names.message);
106            heap.add_property(object, key, PropertyFlags::data(true, false, true), message)?;
107        }
108        Ok(object)
109    })();
110    let result = result.and_then(|object| {
111        cx.heap.roots().push(Value::object(object));
112        add_stack_at(cx, object, false, location, omit).map(|()| Value::object(object))
113    });
114    cx.heap.roots().truncate(mark);
115    result
116}
117
118/// An error object of `kind` with neither message nor `stack`: QuickJS's
119/// internal `AggregateError` of `Promise.any`, made by
120/// `JS_NewObjectProtoClass` without a backtrace.
121pub(crate) fn new_bare_error(cx: &mut Cx<'_>, kind: ErrorKind) -> Result<Value, HeapError> {
122    let prototype = cx.realm.intrinsic(kind.prototype());
123    let object = cx
124        .heap
125        .alloc_object(crate::ObjectKind::Ordinary, Some(prototype), 1)?;
126    cx.heap
127        .set_object_flag(object, crate::heap::object::flags::ERROR);
128    Ok(Value::object(object))
129}
130
131/// Most frames a backtrace lists.
132const BACKTRACE_FRAMES: usize = 64;
133
134/// QuickJS's `build_backtrace`: the bytecode frames, innermost first, as
135/// `    at name (file:line:column)` lines in the own `stack` property of
136/// `object`, which the caller roots. A frame whose current instruction is
137/// unknown (below a re-entry from Rust) has no position.
138///
139/// A native running (called from the innermost frame) is listed first as
140/// `    at name (native)`, unless `skip_native` (an Error constructor, which
141/// QuickJS's `BACKTRACE_FLAG_SKIP_FIRST_LEVEL` leaves out). When the
142/// innermost frame's instruction is unknown (the loop itself throws), the
143/// loop's unwind fills its position in later ([`locate_first_frame`]).
144pub(crate) fn add_stack(
145    cx: &mut Cx<'_>,
146    object: ObjectId,
147    skip_native: bool,
148) -> Result<(), HeapError> {
149    add_stack_at(cx, object, skip_native, None, 0)
150}
151
152/// [`add_stack`] after QuickJS's `build_backtrace` with a `filename`: the
153/// location's own `fileName`, `lineNumber` and `columnNumber` (writable,
154/// configurable), and the backtrace's first line; the running frames follow
155/// but the `omit` innermost (`usize::MAX`: an error of a source no frame
156/// runs; one: an error of a script's declaration prologue, which QuickJS
157/// checks in `js_closure2` before the script's frame exists).
158pub(crate) fn add_stack_at(
159    cx: &mut Cx<'_>,
160    object: ObjectId,
161    skip_native: bool,
162    location: Option<Location<'_>>,
163    omit: usize,
164) -> Result<(), HeapError> {
165    let mut text = String::new();
166    if let Some(Location { file, line, column }) = location {
167        let _ = writeln!(text, "    at {file}:{line}:{column}");
168        let names = cx.realm.names;
169        let flags = PropertyFlags::data(true, false, true);
170        let file = cx.heap.alloc_string(file)?;
171        cx.heap.add_property(
172            object,
173            Names::key(names.file_name),
174            flags,
175            Value::string(file),
176        )?;
177        cx.heap.add_property(
178            object,
179            Names::key(names.line_number),
180            flags,
181            Value::int32(line as i32),
182        )?;
183        cx.heap.add_property(
184            object,
185            Names::key(names.column_number),
186            flags,
187            Value::int32(column as i32),
188        )?;
189    }
190    let vm = &cx.heap.vm;
191    let len = vm.frames.len();
192    let top = len.saturating_sub(omit);
193    let mut line_index = u32::from(location.is_some());
194    let eval = Value::object(cx.realm.intrinsic(crate::Intrinsic::Eval));
195    let mut sites = vm.natives.iter().rev().peekable();
196    while sites.next_if(|site| site.depth as usize > top).is_some() {}
197    let mut unpositioned = Vec::new();
198    for index in (0..top).rev().take(BACKTRACE_FRAMES) {
199        let frame = &vm.frames[index];
200        let unit = cx.unit(frame.unit);
201        // The natives this frame called and which are still running (one
202        // the loop called, then those it called from Rust): their lines,
203        // innermost first, then this frame at the loop's call.
204        let mut site_pc = None;
205        let mut matched = false;
206        while let Some(site) = sites.next_if(|site| site.depth as usize == index + 1) {
207            let direct_eval =
208                site.callee == eval && unit.direct_eval_sites.binary_search(&site.pc).is_ok();
209            // A site with no callee only places the frame (an async
210            // function's body entered from its call).
211            if let Some(callee) = site.callee.as_object()
212                && !(skip_native && index + 1 == len && !matched)
213                && !direct_eval
214            {
215                let name = native_name(cx, callee);
216                let name = if name.is_empty() {
217                    "<anonymous>"
218                } else {
219                    &name
220                };
221                let _ = writeln!(text, "    at {name} (native)");
222                line_index += 1;
223            }
224            matched = true;
225            site_pc = (site.pc != super::NO_PC).then_some(site.pc);
226        }
227        let pc = match site_pc {
228            Some(pc) => Some(pc),
229            None if index + 1 == len => None,
230            None => {
231                let above = &vm.frames[index + 1];
232                (above.flags & frame_flags::ENTRY == 0 && above.return_pc > 0)
233                    .then(|| above.return_pc - 1)
234            }
235        };
236        if pc.is_none() {
237            unpositioned.push((index as u32 + 1, line_index));
238        }
239        line_index += 1;
240        let debug = &unit.debug;
241        // QuickJS reads the running function's `name` (`get_prop_string`);
242        // its bytecode functions all have one, which a script function here
243        // lacks: the unit's name stands in.
244        let key = Names::key(cx.realm.names.name);
245        let name =
246            if cx.heap.lookup_own(frame.function, key).is_none() && !debug_name(&unit).is_empty() {
247                debug_name(&unit).to_owned()
248            } else {
249                native_name(cx, frame.function)
250            };
251        let name = if name.is_empty() {
252            "<anonymous>"
253        } else {
254            &name
255        };
256        // A unit compiled without debug information (`-s`) has no file, and
257        // QuickJS's line then stops at the name.
258        let _ = match (&debug.filename, pc.and_then(|pc| debug.position(pc))) {
259            (None, _) => writeln!(text, "    at {name}"),
260            (Some(file), Some((line, column))) => {
261                writeln!(text, "    at {name} ({file}:{line}:{column})")
262            }
263            (Some(file), None) => writeln!(text, "    at {name} ({file})"),
264        };
265        // Entered through a rewritten `call` or `apply`: the native QuickJS ran
266        // between this frame and its caller.
267        if frame.flags & frame_flags::VIA_CALL != 0 {
268            let _ = writeln!(text, "    at call (native)");
269            line_index += 1;
270        } else if frame.flags & frame_flags::VIA_APPLY != 0 {
271            let _ = writeln!(text, "    at apply (native)");
272            line_index += 1;
273        } else if frame.flags & frame_flags::VIA_CHAIN != 0 {
274            let vias = vm
275                .via
276                .frames
277                .iter()
278                .rev()
279                .filter(|(at, _)| *at as usize == index);
280            for &(_, via) in vias {
281                let name = via
282                    .as_object()
283                    .map(|via| native_name(cx, via))
284                    .unwrap_or_default();
285                let _ = writeln!(text, "    at {name} (native)");
286                line_index += 1;
287            }
288        }
289    }
290    let text = cx.heap.alloc_string(&text)?;
291    let key = Names::key(cx.realm.names.stack);
292    cx.heap.add_property(
293        object,
294        key,
295        PropertyFlags::data(true, false, true),
296        Value::string(text),
297    )?;
298    cx.heap.vm.unpositioned = (!unpositioned.is_empty()).then_some((object, unpositioned));
299    Ok(())
300}
301
302fn debug_name(unit: &crate::bytecode::CodeUnit) -> &str {
303    &unit.debug.name
304}
305
306/// A function's `name` for its backtrace line, as QuickJS's
307/// `get_prop_string` reads it: an own data property, else the prototype's
308/// (one level); empty unless a string.
309fn native_name(cx: &Cx<'_>, function: ObjectId) -> String {
310    let key = Names::key(cx.realm.names.name);
311    let read = |object: ObjectId| {
312        cx.heap.lookup_own(object, key).map(|(slot, flags)| {
313            if flags.has(PropertyFlags::ACCESSOR) {
314                String::new()
315            } else {
316                // A rope (`JS_TAG_STRING_ROPE`) reads as absent.
317                cx.heap
318                    .get_slot(object, slot)
319                    .as_string()
320                    .filter(|id| cx.heap.strings.get(id.0).flat().is_some())
321                    .map(|id| cx.heap.string_text(id))
322                    .unwrap_or_default()
323            }
324        })
325    };
326    read(function)
327        .or_else(|| cx.heap.prototype_of(function).and_then(read))
328        .unwrap_or_default()
329}
330
331/// Where the throw of `thrown` passes through the frame at the top of the
332/// stack, at instruction `pc` of `unit`: places that frame's backtrace line
333/// if [`add_stack`] could not.
334#[cold]
335#[inline(never)]
336pub(crate) fn place_frame(
337    cx: &mut Cx<'_>,
338    thrown: Value,
339    unit: &crate::bytecode::CodeUnit,
340    pc: u32,
341) {
342    let depth = cx.heap.vm.frames.len() as u32;
343    let Some((error, entries)) = &mut cx.heap.vm.unpositioned else {
344        return;
345    };
346    if thrown.as_object() != Some(*error) {
347        return;
348    }
349    let error = *error;
350    let Some(entry) = entries.iter().position(|&(frame, _)| frame == depth) else {
351        return;
352    };
353    let (_, line) = entries.swap_remove(entry);
354    locate_line(cx, error, unit, pc, line as usize);
355}
356
357/// Puts the line and column of instruction `pc` of `unit` into line `index`
358/// of `error`'s backtrace (`    at name (file)` becomes
359/// `    at name (file:line:column)`).
360fn locate_line(
361    cx: &mut Cx<'_>,
362    error: ObjectId,
363    unit: &crate::bytecode::CodeUnit,
364    pc: u32,
365    index: usize,
366) {
367    let Some((line, column)) = unit.debug.position(pc) else {
368        return;
369    };
370    let key = Names::key(cx.realm.names.stack);
371    let Some((slot, flags)) = cx.heap.lookup_own(error, key) else {
372        return;
373    };
374    if flags.has(PropertyFlags::ACCESSOR) {
375        return;
376    }
377    let Some(id) = cx.heap.get_slot(error, slot).as_string() else {
378        return;
379    };
380    let text = cx.heap.string_text(id);
381    let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
382    let Some(target) = lines.get_mut(index) else {
383        return;
384    };
385    let Some(head) = target.strip_suffix(')') else {
386        return;
387    };
388    if head.ends_with("native")
389        || head
390            .rsplit('(')
391            .next()
392            .is_some_and(|file| file.contains(':'))
393    {
394        return;
395    }
396    *target = format!("{head}:{line}:{column})");
397    let located = lines.join("\n");
398    let mark = cx.heap.roots().mark();
399    cx.heap.roots().push(Value::object(error));
400    if let Ok(located) = cx.heap.alloc_string(&located) {
401        cx.heap.set_slot(error, slot, Value::string(located));
402    }
403    cx.heap.roots().truncate(mark);
404}
405
406/// The thrown value for an allocation failure.
407#[inline(never)]
408pub(crate) fn out_of_memory(cx: &Cx<'_>) -> Throw {
409    oom(cx.realm)
410}
411
412/// [`out_of_memory`] with only the realm borrowed.
413pub(crate) fn oom(realm: &Realm) -> Throw {
414    Throw(Value::object(realm.intrinsic(Intrinsic::OutOfMemoryError)))
415}
416
417#[inline(never)]
418pub(crate) fn heap_error(cx: &Cx<'_>, _: HeapError) -> Throw {
419    out_of_memory(cx)
420}
421
422/// A new `kind` error with `message`, formatted as QuickJS's
423/// `JS_ThrowError2` buffer leaves it ([`buffered_message`]).
424#[inline(never)]
425pub(crate) fn throw(cx: &mut Cx<'_>, kind: ErrorKind, message: &str) -> Throw {
426    let units: Vec<u16> = message.encode_utf16().collect();
427    throw_units(cx, kind, &units)
428}
429
430/// QuickJS's `JS_ThrowError2` message: the formatted text through a
431/// 256-byte `vsnprintf` buffer (255 bytes of its WTF-8, cut at a NUL), read
432/// back by `JS_NewString`, which turns a sequence the cut split into one
433/// U+FFFD.
434pub(crate) fn buffered_message(units: &[u16]) -> Vec<u16> {
435    const BUFFER: usize = 255;
436    let mut out = Vec::with_capacity(units.len().min(BUFFER));
437    let mut used = 0;
438    let mut index = 0;
439    while index < units.len() {
440        let unit = units[index];
441        if unit == 0 {
442            break;
443        }
444        let pair = (0xd800..0xdc00).contains(&unit)
445            && units
446                .get(index + 1)
447                .is_some_and(|next| (0xdc00..0xe000).contains(next));
448        let (bytes, count) = match unit {
449            _ if pair => (4, 2),
450            0..0x80 => (1, 1),
451            0x80..0x800 => (2, 1),
452            _ => (3, 1),
453        };
454        if used + bytes > BUFFER {
455            if used < BUFFER {
456                out.push(0xfffd);
457            }
458            break;
459        }
460        out.extend_from_slice(&units[index..index + count]);
461        used += bytes;
462        index += count;
463    }
464    out
465}
466
467/// [`throw`] of a message given as UTF-16 units (a name with a lone
468/// surrogate keeps it).
469#[inline(never)]
470pub(crate) fn throw_units(cx: &mut Cx<'_>, kind: ErrorKind, units: &[u16]) -> Throw {
471    let units = buffered_message(units);
472    let result = cx
473        .heap
474        .alloc_string_units(&units)
475        .and_then(|text| new_error(cx, kind, Value::string(text)));
476    match result {
477        Ok(error) => Throw(error),
478        Err(_) => out_of_memory(cx),
479    }
480}
481
482/// A new `kind` error with `message` thrown by a script's declaration
483/// prologue: its backtrace leaves the script's own frame out, as QuickJS
484/// checks the declarations before it runs the script.
485#[cold]
486#[inline(never)]
487pub(crate) fn throw_prologue(cx: &mut Cx<'_>, kind: ErrorKind, message: &str) -> Throw {
488    let result = cx
489        .heap
490        .alloc_string(message)
491        .and_then(|text| new_error_at(cx, kind, Value::string(text), None, 1));
492    match result {
493        Ok(error) => Throw(error),
494        Err(_) => out_of_memory(cx),
495    }
496}
497
498/// A new `kind` error with `message` located at `line` and `column` of
499/// `file` (a module's source that no frame runs): its `stack` is the
500/// location alone.
501#[inline(never)]
502pub(crate) fn throw_at(
503    cx: &mut Cx<'_>,
504    kind: ErrorKind,
505    message: &str,
506    file: &str,
507    line: u32,
508    column: u32,
509) -> Throw {
510    located(cx, kind, message, Location { file, line, column }, false)
511}
512
513/// A new `kind` error located at `file:line:column` (where a compile
514/// error is), the running frames' backtrace after the location.
515#[inline(never)]
516pub(crate) fn throw_located(
517    cx: &mut Cx<'_>,
518    kind: ErrorKind,
519    message: &str,
520    file: &str,
521    line: u32,
522    column: u32,
523) -> Throw {
524    located(cx, kind, message, Location { file, line, column }, true)
525}
526
527fn located(
528    cx: &mut Cx<'_>,
529    kind: ErrorKind,
530    message: &str,
531    location: Location<'_>,
532    frames: bool,
533) -> Throw {
534    let result = cx.heap.alloc_string(message).and_then(|text| {
535        let omit = if frames { 0 } else { usize::MAX };
536        new_error_at(cx, kind, Value::string(text), Some(location), omit)
537    });
538    match result {
539        Ok(error) => Throw(error),
540        Err(_) => out_of_memory(cx),
541    }
542}
543
544#[inline(never)]
545pub(crate) fn type_error(cx: &mut Cx<'_>, message: &str) -> Throw {
546    throw(cx, ErrorKind::Type, message)
547}
548
549#[inline(never)]
550pub(crate) fn range_error(cx: &mut Cx<'_>, message: &str) -> Throw {
551    throw(cx, ErrorKind::Range, message)
552}
553
554#[inline(never)]
555pub(crate) fn stack_overflow(cx: &mut Cx<'_>) -> Throw {
556    throw(cx, ErrorKind::Internal, "stack overflow")
557}
558
559/// The poll the loop runs when its countdown reaches zero: the embedder's
560/// flag, then its hook. An interrupt sets `vm.interrupted`, which makes the
561/// throw uncatchable, and polls again at every later back-edge and call so
562/// any JavaScript a native runs on the way out stops too.
563#[cold]
564#[inline(never)]
565pub(crate) fn poll_interrupts(cx: &mut Cx<'_>) -> Result<(), Throw> {
566    let interrupt = cx.heap.vm.interrupted
567        || cx.host.interrupt.load(std::sync::atomic::Ordering::Relaxed)
568        || cx.host.hooks.interrupt();
569    if !interrupt {
570        cx.heap.vm.poll = super::INTERRUPT_INTERVAL;
571        return Ok(());
572    }
573    cx.heap.vm.interrupted = true;
574    cx.heap.vm.poll = 1;
575    Err(throw(cx, ErrorKind::Internal, "interrupted"))
576}
577
578/// Whether a throw may be caught or turned into a completion (a rejection,
579/// a reported job result): not while an interrupt unwinds.
580#[inline]
581pub(crate) fn catchable(cx: &Cx<'_>) -> bool {
582    !cx.heap.vm.interrupted
583}
584
585/// QuickJS's `JS_ThrowTypeErrorNotAConstructor`: a callable `value` whose
586/// `name` (an own string data property, else its prototype's) reads without
587/// running code is named, `" is not a constructor"` for an empty name.
588#[cold]
589#[inline(never)]
590pub(crate) fn not_a_constructor(cx: &mut Cx<'_>, value: Value) -> Throw {
591    match function_name_units(cx, value) {
592        Some(mut name) => {
593            name.extend(" is not a constructor".encode_utf16());
594            throw_units(cx, ErrorKind::Type, &name)
595        }
596        None => type_error(cx, "not a constructor"),
597    }
598}
599
600/// QuickJS's `JS_AtomGetStr` into its 64-byte buffer, as a `%s` of a key
601/// prints: an ASCII name whole (to a NUL), any other a unit at a time
602/// (UTF-8, a surrogate on its own) while 58 bytes are used at most, and
603/// an index in decimal.
604pub(crate) fn atom_units(heap: &crate::Heap, key: crate::PropertyKey) -> Vec<u16> {
605    let units: Vec<u16> = match key.kind() {
606        crate::KeyKind::String(id) => heap.string(id).repr().units().collect(),
607        _ => {
608            return super::property::atom_text(heap, key)
609                .encode_utf16()
610                .collect();
611        }
612    };
613    let mut out = Vec::new();
614    if units.iter().all(|&unit| unit < 0x80) {
615        out = units;
616    } else {
617        const LIMIT: usize = 64 - 6;
618        let mut used = 0;
619        for &unit in &units {
620            if used >= LIMIT {
621                break;
622            }
623            used += match unit {
624                0..0x80 => 1,
625                0x80..0x800 => 2,
626                _ => 3,
627            };
628            out.push(unit);
629        }
630    }
631    // `%s` stops at a NUL.
632    if let Some(end) = out.iter().position(|&unit| unit == 0) {
633        out.truncate(end);
634    }
635    out
636}
637
638/// `{before}{key}{after}` with the key as [`atom_units`] prints it.
639pub(crate) fn atom_message(
640    heap: &crate::Heap,
641    before: &str,
642    key: crate::PropertyKey,
643    after: &str,
644) -> Vec<u16> {
645    let mut units: Vec<u16> = before.encode_utf16().collect();
646    units.extend(atom_units(heap, key));
647    units.extend(after.encode_utf16());
648    units
649}
650
651/// QuickJS's `JS_CallConstructor` failure on `value`: a non-object is
652/// `not a function`, an object [`not_a_constructor`].
653#[cold]
654#[inline(never)]
655pub(crate) fn construct_error(cx: &mut Cx<'_>, value: Value) -> Throw {
656    if value.as_object().is_none() {
657        return type_error(cx, "not a function");
658    }
659    not_a_constructor(cx, value)
660}
661
662/// QuickJS's `get_prop_string(func, "name")` on a callable `value`, cut at
663/// the first NUL as `%s` prints it.
664fn function_name_units(cx: &Cx<'_>, value: Value) -> Option<Vec<u16>> {
665    let object = value.as_object()?;
666    let heap = &cx.heap;
667    if !heap
668        .header(object)
669        .has(crate::heap::object::flags::CALLABLE)
670        || heap.object_kind(object) == crate::heap::object::ObjectKind::Proxy
671    {
672        return None;
673    }
674    let key = Names::key(cx.realm.names.name);
675    let read = |object: ObjectId| -> Option<Option<Vec<u16>>> {
676        let (slot, flags) = heap.lookup_own(object, key)?;
677        if flags.has(PropertyFlags::ACCESSOR) {
678            return Some(None);
679        }
680        // `get_prop_string` takes only `JS_TAG_STRING`: a rope reads as
681        // absent.
682        Some(
683            heap.get_slot(object, slot)
684                .as_string()
685                .filter(|id| heap.strings.get(id.0).flat().is_some())
686                .map(|id| heap.string(id).repr().units().collect()),
687        )
688    };
689    let units = match read(object) {
690        Some(found) => found,
691        None => heap.prototype_of(object).and_then(read).flatten(),
692    }?;
693    let end = units
694        .iter()
695        .position(|&unit| unit == 0)
696        .unwrap_or(units.len());
697    Some(units[..end].to_vec())
698}
699
700/// `"{class} object expected"`, QuickJS's `JS_ThrowTypeErrorInvalidClass`.
701#[cold]
702#[inline(never)]
703pub(crate) fn object_expected(cx: &mut Cx<'_>, class: &str) -> Throw {
704    type_error(cx, &format!("{class} object expected"))
705}
706
707/// InternalError `string too long`, QuickJS's string-building overflow.
708#[cold]
709#[inline(never)]
710pub(crate) fn string_too_long(cx: &mut Cx<'_>) -> Throw {
711    throw(cx, ErrorKind::Internal, "string too long")
712}
713
714/// QuickJS's `JS_GetIterator` on `null` or `undefined`: the `@@iterator`
715/// read fails.
716#[cold]
717#[inline(never)]
718pub(crate) fn not_iterable_nullish(cx: &mut Cx<'_>, value: Value) -> Throw {
719    let base = if value.is_null() { "null" } else { "undefined" };
720    type_error(
721        cx,
722        &format!("cannot read property 'Symbol.iterator' of {base}"),
723    )
724}