Skip to main content

oxide_core/vm/
print.rs

1//! QuickJS's `JS_PrintValue`: what `print`, `console.log` and an
2//! uncaught-exception dump show for a value. It runs no JavaScript: no
3//! accessor, proxy trap, coercion or lazy intrinsic. It matches the pinned
4//! QuickJS.
5
6use super::Cx;
7use crate::heap::atom::{KeyKind, PropertyKey};
8use crate::heap::object::{InternalData, InternalTag, NONE, ObjectKind, flags};
9use crate::runtime::{Names, Runtime};
10use crate::shape::PropertyFlags;
11use crate::value::{Kind, ObjectId, StringId, Value};
12
13const MAX_DEPTH: usize = 2;
14const MAX_STRING_LENGTH: usize = 1_000;
15const MAX_ITEM_COUNT: usize = 100;
16
17#[derive(Clone, Copy, PartialEq, Eq)]
18enum Comma {
19    /// Nothing printed yet.
20    Empty,
21    /// The next item needs `, `.
22    Between,
23    /// A `[Function f]`-style prefix: the next item opens ` { `.
24    Suffix,
25}
26
27enum Body {
28    Ordinary,
29    /// A dense array: its leading elements, then empty items to `length`.
30    /// A sparse one (`sparse`) prints its elements as properties, inside the
31    /// brackets.
32    Array {
33        dense: Vec<Value>,
34        length: u32,
35        sparse: bool,
36    },
37    TypedArray,
38    Function,
39    /// The entries in order, `None` for a zombie (`Table::is_zombie`).
40    Map(Vec<Option<(Value, Value)>>, usize),
41    Set(Vec<Option<Value>>, usize),
42    RegExp,
43    Date(f64),
44    Error,
45}
46
47enum Slot {
48    Data(Value),
49    Accessor { get: bool, set: bool },
50}
51
52struct Printer<'a, 'c> {
53    cx: &'a mut Cx<'c>,
54    out: Out,
55    stack: Vec<ObjectId>,
56}
57
58/// The printer's output: UTF-8 text, with the C strings QuickJS writes raw
59/// (names, messages, stacks) as WTF-8.
60struct Out(Vec<u8>);
61
62impl Out {
63    fn push_str(&mut self, text: &str) {
64        self.0.extend_from_slice(text.as_bytes());
65    }
66
67    fn push(&mut self, character: char) {
68        let mut buffer = [0; 4];
69        self.push_str(character.encode_utf8(&mut buffer));
70    }
71
72    fn extend(&mut self, characters: impl Iterator<Item = char>) {
73        for character in characters {
74            self.push(character);
75        }
76    }
77}
78
79/// UTF-16 code units as QuickJS's `JS_ToCStringLen` encodes them: UTF-8,
80/// a lone surrogate as its own three bytes (WTF-8).
81pub(crate) fn wtf8(units: impl Iterator<Item = u16>, out: &mut Vec<u8>) {
82    for point in char::decode_utf16(units) {
83        match point {
84            Ok(point) => {
85                let mut buffer = [0; 4];
86                out.extend_from_slice(point.encode_utf8(&mut buffer).as_bytes());
87            }
88            Err(error) => {
89                let unit = error.unpaired_surrogate();
90                out.extend_from_slice(&[
91                    0xE0 | (unit >> 12) as u8,
92                    0x80 | ((unit >> 6) & 0x3F) as u8,
93                    0x80 | (unit & 0x3F) as u8,
94                ]);
95            }
96        }
97    }
98}
99
100impl Runtime {
101    /// The value as QuickJS's `JS_PrintValue` prints it; a string is quoted
102    /// and escaped (`print` writes a top-level string raw instead). A lone
103    /// surrogate in a raw C string shows as U+FFFD; [`Self::print_value_bytes`]
104    /// keeps it.
105    pub fn print_value(&mut self, value: Value) -> String {
106        self.native_cx().print_value(value)
107    }
108
109    /// [`Self::print_value`] as the bytes QuickJS writes (WTF-8).
110    pub fn print_value_bytes(&mut self, value: Value) -> Vec<u8> {
111        self.native_cx().print_value_bytes(value)
112    }
113}
114
115impl crate::NativeCx<'_> {
116    /// [`Runtime::print_value`].
117    pub fn print_value(&mut self, value: Value) -> String {
118        String::from_utf8_lossy(&print_value(&mut self.cx(), value)).into_owned()
119    }
120
121    /// [`Runtime::print_value_bytes`].
122    pub fn print_value_bytes(&mut self, value: Value) -> Vec<u8> {
123        print_value(&mut self.cx(), value)
124    }
125}
126
127/// [`Runtime::print_value_bytes`] from inside the engine (`print`).
128pub(crate) fn print_value(cx: &mut Cx<'_>, value: Value) -> Vec<u8> {
129    let mut printer = Printer {
130        cx,
131        out: Out(Vec::new()),
132        stack: Vec::with_capacity(MAX_DEPTH),
133    };
134    printer.value(value);
135    printer.out.0
136}
137
138impl Printer<'_, '_> {
139    fn value(&mut self, value: Value) {
140        match value.kind() {
141            Kind::Undefined => self.out.push_str("undefined"),
142            Kind::Null => self.out.push_str("null"),
143            Kind::Bool => self.out.push_str(if value.as_bool() == Some(true) {
144                "true"
145            } else {
146                "false"
147            }),
148            Kind::Int32 | Kind::Number => {
149                let number = value.as_f64().unwrap_or(f64::NAN);
150                if number == 0.0 && number.is_sign_negative() {
151                    self.out.push_str("-0");
152                } else {
153                    self.out.push_str(&super::number::to_string(number));
154                }
155            }
156            Kind::BigInt => {
157                let text = super::ops::big(self.cx.heap, value).to_string_radix10();
158                self.out.push_str(&text);
159                self.out.push('n');
160            }
161            Kind::String => self.string(value.as_string().expect("a string")),
162            Kind::Symbol => {
163                self.out.push_str("Symbol(");
164                let record = self.cx.heap.symbol(value.as_symbol().expect("a symbol"));
165                let units = if record.description == NONE {
166                    Vec::new()
167                } else {
168                    self.units(StringId(record.description))
169                };
170                self.atom_units(&units);
171                self.out.push(')');
172            }
173            Kind::Object => self.object(value.as_object().expect("an object")),
174            _ => self.out.push_str("uninitialized"),
175        }
176    }
177
178    fn units(&self, id: StringId) -> Vec<u16> {
179        self.cx.heap.string(id).repr().units().collect()
180    }
181
182    /// The leaves of `id` left to right without flattening it: QuickJS
183    /// prints a rope leaf by leaf, so a surrogate pair split across two
184    /// leaves prints as two escapes.
185    fn leaves(&self, id: StringId) -> Vec<Vec<u16>> {
186        let heap = &*self.cx.heap;
187        let mut leaves = Vec::new();
188        let mut stack = vec![id];
189        while let Some(id) = stack.pop() {
190            let record = heap.strings.get(id.0);
191            match (record.flat(), heap.ropes.get(&id.0)) {
192                (Some(flat), _) => leaves.push(flat.units().collect()),
193                (None, Some(&(left, right))) => {
194                    stack.push(right);
195                    stack.push(left);
196                }
197                (None, None) => {}
198            }
199        }
200        leaves
201    }
202
203    fn string(&mut self, id: StringId) {
204        let leaves = self.leaves(id);
205        let total: usize = leaves.iter().map(Vec::len).sum();
206        self.out.push('"');
207        let mut position = 0;
208        for leaf in &leaves {
209            if position < MAX_STRING_LENGTH {
210                let shown = leaf.len().min(MAX_STRING_LENGTH - position);
211                self.escaped(&leaf[..shown], '"');
212            }
213            position += leaf.len();
214        }
215        self.out.push('"');
216        if total > MAX_STRING_LENGTH {
217            let remaining = total - MAX_STRING_LENGTH;
218            self.out
219                .push_str(&format!("... {remaining} more character"));
220            if remaining > 1 {
221                self.out.push('s');
222            }
223        }
224    }
225
226    fn escaped(&mut self, units: &[u16], separator: char) {
227        let mut index = 0;
228        while index < units.len() {
229            let unit = units[index];
230            index += 1;
231            match unit {
232                0x09 => self.out.push_str("\\t"),
233                0x0d => self.out.push_str("\\r"),
234                0x0a => self.out.push_str("\\n"),
235                0x08 => self.out.push_str("\\b"),
236                0x0c => self.out.push_str("\\f"),
237                0x5c => self.out.push_str("\\\\"),
238                _ if unit == separator as u16 => {
239                    self.out.push('\\');
240                    self.out.push(separator);
241                }
242                0x20..=0x7e => self.out.push(unit as u8 as char),
243                0x00..=0x1f | 0x7f..=0x9f => self.unicode_escape(unit),
244                0xd800..=0xdbff => match units.get(index) {
245                    Some(&low) if (0xdc00..=0xdfff).contains(&low) => {
246                        index += 1;
247                        let code = 0x1_0000
248                            + ((u32::from(unit) - 0xd800) << 10)
249                            + (u32::from(low) - 0xdc00);
250                        self.out.push(char::from_u32(code).unwrap_or('\u{fffd}'));
251                    }
252                    _ => self.unicode_escape(unit),
253                },
254                0xdc00..=0xdfff => self.unicode_escape(unit),
255                _ => self
256                    .out
257                    .push(char::from_u32(u32::from(unit)).unwrap_or('\u{fffd}')),
258            }
259        }
260    }
261
262    fn unicode_escape(&mut self, unit: u16) {
263        self.out.push_str(&format!("\\u{unit:04x}"));
264    }
265
266    /// A property name or symbol description: bare when it is an
267    /// identifier of ASCII letters, `$`, `_` and digits, else quoted.
268    fn atom_units(&mut self, units: &[u16]) {
269        let identifier = !units.is_empty()
270            && units.iter().enumerate().all(|(index, &unit)| {
271                matches!(unit, 0x61..=0x7a | 0x41..=0x5a | 0x5f | 0x24)
272                    || (index > 0 && (0x30..=0x39).contains(&unit))
273            });
274        if identifier {
275            self.out
276                .extend(units.iter().map(|&unit| unit as u8 as char));
277        } else {
278            self.out.push('"');
279            self.escaped(units, '"');
280            self.out.push('"');
281        }
282    }
283
284    fn key(&mut self, key: PropertyKey) {
285        match key.kind() {
286            KeyKind::Index(index) => self.out.push_str(&index.to_string()),
287            KeyKind::String(id) => {
288                let units = self.units(id);
289                match self.cx.heap.string(id).array_index {
290                    NONE => self.atom_units(&units),
291                    index => self.out.push_str(&index.to_string()),
292                }
293            }
294            KeyKind::Symbol(id) => {
295                let record = self.cx.heap.symbol(id);
296                let units = if record.description == NONE {
297                    Vec::new()
298                } else {
299                    self.units(StringId(record.description))
300                };
301                self.atom_units(&units);
302            }
303        }
304    }
305
306    fn class_name(&self, object: ObjectId) -> &'static str {
307        let heap = &self.cx.heap;
308        match heap.object_kind(object) {
309            ObjectKind::Array => "Array",
310            ObjectKind::Function | ObjectKind::BoundFunction => self.function_class(object),
311            ObjectKind::Arguments => "Arguments",
312            ObjectKind::RegExp => "RegExp",
313            ObjectKind::ForInIterator => "ForInIterator",
314            ObjectKind::StringWrapper => "String",
315            ObjectKind::NumberWrapper => "Number",
316            ObjectKind::BooleanWrapper => "Boolean",
317            ObjectKind::SymbolWrapper => "Symbol",
318            ObjectKind::BigIntWrapper => "BigInt",
319            ObjectKind::Date => "Date",
320            ObjectKind::TypedArray => super::typed::view(heap, object).0.kind.name(),
321            ObjectKind::ModuleNamespace => "Module",
322            ObjectKind::Builtin => match heap.internal_tag(object) {
323                InternalTag::ArrayIterator => "Array Iterator",
324                InternalTag::StringIterator => "String Iterator",
325                InternalTag::MapIterator => "Map Iterator",
326                InternalTag::SetIterator => "Set Iterator",
327                InternalTag::RegExpStringIterator => "RegExp String Iterator",
328                InternalTag::IteratorHelper => "Iterator Helper",
329                InternalTag::Iterator => "Iterator",
330                InternalTag::WrapForValidIterator => "Iterator Wrap",
331                InternalTag::AsyncFromSyncIterator => "",
332                InternalTag::Promise => "Promise",
333                InternalTag::Map => "Map",
334                InternalTag::Set => "Set",
335                InternalTag::WeakMap => "WeakMap",
336                InternalTag::WeakSet => "WeakSet",
337                InternalTag::WeakRef => "WeakRef",
338                InternalTag::FinalizationRegistry => "FinalizationRegistry",
339                InternalTag::Generator => "Generator",
340                InternalTag::AsyncGenerator => "AsyncGenerator",
341                InternalTag::AsyncFunction => "AsyncFunction",
342                InternalTag::ArrayBuffer => "ArrayBuffer",
343                InternalTag::SharedArrayBuffer => "SharedArrayBuffer",
344                InternalTag::DataView => "DataView",
345                _ => "Object",
346            },
347            _ if heap.header(object).has(flags::ERROR) => "Error",
348            _ => "Object",
349        }
350    }
351
352    fn function_class(&self, object: ObjectId) -> &'static str {
353        let heap = &self.cx.heap;
354        if heap.object_kind(object) != ObjectKind::Function {
355            return "Function";
356        }
357        let Some(bytecode) = heap.function_bytecode(object) else {
358            // QuickJS's resolving functions have their own classes.
359            let native = heap.function_record(object).native;
360            return if native == crate::builtins::Native::PromiseResolveFunction.index() {
361                "PromiseResolveFunction"
362            } else if native == crate::builtins::Native::PromiseRejectFunction.index() {
363                "PromiseRejectFunction"
364            } else {
365                "Function"
366            };
367        };
368        let unit = self.cx.unit(heap.bytecode_unit(bytecode));
369        use crate::bytecode::flags as unit_flags;
370        match (unit.has(unit_flags::ASYNC), unit.has(unit_flags::GENERATOR)) {
371            (false, false) => "Function",
372            (false, true) => "GeneratorFunction",
373            (true, false) => "AsyncFunction",
374            (true, true) => "AsyncGeneratorFunction",
375        }
376    }
377
378    fn object(&mut self, object: ObjectId) {
379        if let Some(index) = self.stack.iter().position(|&seen| seen == object) {
380            self.out.push_str(&format!("[circular {index}]"));
381            return;
382        }
383        if self.stack.len() >= MAX_DEPTH {
384            let name = self.class_name(object);
385            self.out.push('[');
386            let units: Vec<u16> = name.encode_utf16().collect();
387            self.atom_units(&units);
388            self.out.push(']');
389            return;
390        }
391        let body = self.body(object);
392        let properties = self.properties(object, &body);
393        self.stack.push(object);
394        self.print_object(object, body, properties);
395        self.stack.pop();
396    }
397
398    fn body(&mut self, object: ObjectId) -> Body {
399        let heap = &self.cx.heap;
400        if heap.header(object).has(flags::ERROR) {
401            return Body::Error;
402        }
403        match heap.object_kind(object) {
404            ObjectKind::Array => {
405                let length = heap.array_length(object);
406                if heap.header(object).has(flags::SPARSE) || heap.holey.contains(&object.0) {
407                    return Body::Array {
408                        dense: Vec::new(),
409                        length,
410                        sparse: true,
411                    };
412                }
413                let mut dense = Vec::new();
414                let mut index = 0;
415                while index < length {
416                    let value = heap.get_element(object, index);
417                    if value.is_empty() {
418                        break;
419                    }
420                    dense.push(value);
421                    index += 1;
422                }
423                // A hole followed by an element: a sparse array, printed by
424                // its index properties as QuickJS prints a slow array.
425                if (index..length).any(|later| !heap.get_element(object, later).is_empty()) {
426                    return Body::Array {
427                        dense: Vec::new(),
428                        length,
429                        sparse: true,
430                    };
431                }
432                Body::Array {
433                    dense,
434                    length,
435                    sparse: false,
436                }
437            }
438            ObjectKind::TypedArray => Body::TypedArray,
439            ObjectKind::Function | ObjectKind::BoundFunction => Body::Function,
440            ObjectKind::RegExp => Body::RegExp,
441            ObjectKind::Date => {
442                Body::Date(heap.internal_slot(object, 0).as_f64().unwrap_or(f64::NAN))
443            }
444            ObjectKind::Builtin => match heap.internal_tag(object) {
445                InternalTag::Map | InternalTag::Set => {
446                    let map = heap.internal_tag(object) == InternalTag::Map;
447                    let internal = heap.internal(object);
448                    let InternalData::Table(table) = &internal.data else {
449                        return Body::Ordinary;
450                    };
451                    let width = table.width();
452                    let size = table.len();
453                    let mut entries = Vec::new();
454                    let mut live = 0;
455                    for (index, entry) in internal.slots.chunks(width).enumerate() {
456                        if live >= MAX_ITEM_COUNT {
457                            break;
458                        }
459                        if entry[0].is_empty() {
460                            if table.is_zombie(index) {
461                                entries.push(None);
462                            }
463                            continue;
464                        }
465                        live += 1;
466                        entries.push(Some((
467                            entry[0],
468                            if map { entry[1] } else { Value::UNDEFINED },
469                        )));
470                    }
471                    if map {
472                        Body::Map(entries, size)
473                    } else {
474                        Body::Set(
475                            entries
476                                .into_iter()
477                                .map(|entry| entry.map(|(key, _)| key))
478                                .collect(),
479                            size,
480                        )
481                    }
482                }
483                _ => Body::Ordinary,
484            },
485            _ => Body::Ordinary,
486        }
487    }
488
489    /// The enumerable own properties QuickJS's printer visits: an array's
490    /// dense elements are its body, a sparse array's and an ordinary
491    /// object's elements are properties, then the shape's in order.
492    fn properties(&self, object: ObjectId, body: &Body) -> (Vec<(PropertyKey, Slot)>, usize) {
493        let heap = &self.cx.heap;
494        let mut properties = Vec::new();
495        let mut count = 0;
496        let elements_are_properties =
497            !matches!(body, Body::Array { sparse: false, .. } | Body::TypedArray)
498                && heap.object_kind(object).has_elements()
499                && heap.object_kind(object) != ObjectKind::Arguments;
500        if elements_are_properties {
501            let header = heap.header(object);
502            let length = header.elements().len;
503            let limit = if header.kind == ObjectKind::Array {
504                length.min(header.payload)
505            } else {
506                length
507            };
508            for index in 0..limit {
509                let value = heap.get_element(object, index);
510                if value.is_empty() {
511                    continue;
512                }
513                count += 1;
514                if properties.len() < MAX_ITEM_COUNT
515                    && let Some(key) = PropertyKey::index(index)
516                {
517                    properties.push((key, Slot::Data(value)));
518                }
519            }
520        }
521        for (key, flags, slot) in heap.shape_entries(heap.shape_of(object)) {
522            if !flags.has(PropertyFlags::ENUMERABLE) {
523                continue;
524            }
525            if let KeyKind::Symbol(id) = key.kind()
526                && heap.symbol(id).private
527            {
528                continue;
529            }
530            count += 1;
531            if properties.len() >= MAX_ITEM_COUNT {
532                continue;
533            }
534            let value = if flags.has(PropertyFlags::ACCESSOR) {
535                Slot::Accessor {
536                    get: !heap.get_slot(object, slot).is_undefined(),
537                    set: !heap.get_slot(object, slot + 1).is_undefined(),
538                }
539            } else {
540                Slot::Data(heap.get_slot(object, slot))
541            };
542            properties.push((key, value));
543        }
544        if heap.object_kind(object) == ObjectKind::Arguments
545            && heap.header(object).has(crate::heap::object::flags::SPARSE)
546        {
547            self.slow_arguments(object, &mut properties, &mut count);
548        }
549        (properties, count)
550    }
551
552    /// A converted `arguments` object's index properties, QuickJS's
553    /// ordinary properties once its fast array is gone: the still mapped
554    /// indices join the named ones, indices first.
555    fn slow_arguments(
556        &self,
557        object: ObjectId,
558        properties: &mut Vec<(PropertyKey, Slot)>,
559        count: &mut usize,
560    ) {
561        let heap = &self.cx.heap;
562        for index in 0..crate::vm::arguments::mapped_count(heap, object) {
563            let (Some(cell), Some(key)) = (
564                crate::vm::arguments::mapped_cell(heap, object, index),
565                PropertyKey::index(index),
566            ) else {
567                continue;
568            };
569            if heap.lookup_own(object, key).is_none() {
570                *count += 1;
571                if properties.len() < MAX_ITEM_COUNT {
572                    properties.push((key, Slot::Data(heap.scope_get(cell, 0, 0))));
573                }
574            }
575        }
576        let rank = |key: &PropertyKey| match key.kind() {
577            KeyKind::Index(index) => u64::from(index),
578            _ => u64::MAX,
579        };
580        properties.sort_by_key(|(key, _)| rank(key));
581    }
582
583    fn comma(&mut self, state: &mut Comma) {
584        match *state {
585            Comma::Empty => {}
586            Comma::Between => self.out.push_str(", "),
587            Comma::Suffix => self.out.push_str(" { "),
588        }
589        *state = Comma::Between;
590    }
591
592    fn more_items(&mut self, state: &mut Comma, count: usize) {
593        self.comma(state);
594        self.out.push_str(&format!("... {count} more item"));
595        if count > 1 {
596            self.out.push('s');
597        }
598    }
599
600    fn print_object(
601        &mut self,
602        object: ObjectId,
603        body: Body,
604        (properties, count): (Vec<(PropertyKey, Slot)>, usize),
605    ) {
606        let mut state = Comma::Empty;
607        let mut is_array = false;
608        match body {
609            Body::Array {
610                dense,
611                length,
612                sparse,
613            } => {
614                is_array = true;
615                self.out.push_str("[ ");
616                for &value in dense.iter().take(MAX_ITEM_COUNT) {
617                    self.comma(&mut state);
618                    self.value(value);
619                }
620                if dense.len() > MAX_ITEM_COUNT {
621                    self.more_items(&mut state, dense.len() - MAX_ITEM_COUNT);
622                }
623                if !sparse && (dense.len() as u32) < length {
624                    let empty = length as usize - dense.len();
625                    self.comma(&mut state);
626                    self.out.push_str(&format!("<{empty} empty item"));
627                    if empty > 1 {
628                        self.out.push('s');
629                    }
630                    self.out.push('>');
631                }
632            }
633            Body::TypedArray => {
634                is_array = true;
635                let name = self.class_name(object);
636                let length = super::typed::length(self.cx.heap, object).unwrap_or(0);
637                self.atom_units(&name.encode_utf16().collect::<Vec<_>>());
638                self.out.push_str(&format!("({length}) [ "));
639                let shown = length.min(MAX_ITEM_COUNT);
640                for index in 0..shown {
641                    self.comma(&mut state);
642                    match super::typed::get_element(self.cx, object, index as f64) {
643                        // QuickJS prints a 64-bit element as a C integer,
644                        // without the `n`.
645                        Ok(Some(value)) if value.kind() == Kind::BigInt => {
646                            let text = super::ops::big(self.cx.heap, value).to_string_radix10();
647                            self.out.push_str(&text);
648                        }
649                        Ok(Some(value)) => self.value(value),
650                        _ => self.out.push_str("undefined"),
651                    }
652                }
653                if shown < length {
654                    self.more_items(&mut state, length - shown);
655                }
656            }
657            Body::Function => {
658                self.out.push_str("[Function ");
659                match self.raw_string(object, self.cx.realm.names.name) {
660                    Some(name) if !name.is_empty() => self.c_string(&name),
661                    _ => self.out.push_str("(anonymous)"),
662                }
663                self.out.push(']');
664                state = Comma::Suffix;
665            }
666            Body::Map(entries, size) => {
667                self.out.push_str(&format!("Map({size}) {{ "));
668                let mut live = 0;
669                for &entry in &entries {
670                    self.comma(&mut state);
671                    if let Some((key, value)) = entry {
672                        self.value(key);
673                        self.out.push_str(" => ");
674                        self.value(value);
675                        live += 1;
676                    }
677                }
678                if live < size {
679                    self.more_items(&mut state, size - live);
680                }
681            }
682            Body::Set(entries, size) => {
683                self.out.push_str(&format!("Set({size}) {{ "));
684                let mut live = 0;
685                for &entry in &entries {
686                    self.comma(&mut state);
687                    if let Some(key) = entry {
688                        self.value(key);
689                        live += 1;
690                    }
691                }
692                if live < size {
693                    self.more_items(&mut state, size - live);
694                }
695            }
696            Body::RegExp => {
697                self.regexp(object);
698                state = Comma::Suffix;
699            }
700            Body::Date(time) => match crate::builtins::date_iso_string(self.cx, time) {
701                Some(text) => {
702                    self.out.push_str(&text);
703                    state = Comma::Suffix;
704                }
705                None => self.out.push_str("Date { "),
706            },
707            Body::Error => {
708                self.error(object);
709                state = Comma::Suffix;
710            }
711            Body::Ordinary => {
712                let name = self.class_name(object);
713                let plain =
714                    self.cx.heap.object_kind(object) == ObjectKind::Ordinary && name == "Object";
715                if !plain {
716                    self.atom_units(&name.encode_utf16().collect::<Vec<_>>());
717                    self.out.push(' ');
718                }
719                self.out.push_str("{ ");
720            }
721        }
722        let shown = properties.len();
723        for (key, slot) in properties {
724            self.comma(&mut state);
725            self.key(key);
726            self.out.push_str(": ");
727            match slot {
728                Slot::Data(value) => self.value(value),
729                Slot::Accessor { get, set } => self.out.push_str(match (get, set) {
730                    (true, true) => "[Getter/Setter]",
731                    (false, true) => "[Setter]",
732                    _ => "[Getter]",
733                }),
734            }
735        }
736        if shown < count {
737            self.more_items(&mut state, count - shown);
738        }
739        if is_array {
740            self.out.push_str(" ]");
741        } else if state != Comma::Suffix {
742            self.out.push_str(" }");
743        }
744    }
745
746    /// An own data property's string, else the prototype's (one level, as
747    /// the pinned printer reads `name` and `message`).
748    fn raw_string(&self, object: ObjectId, name: StringId) -> Option<Vec<u16>> {
749        let heap = &self.cx.heap;
750        let key = Names::key(name);
751        let read = |object: ObjectId| -> Option<Option<Vec<u16>>> {
752            let (slot, flags) = heap.lookup_own(object, key)?;
753            if flags.has(PropertyFlags::ACCESSOR) {
754                return Some(None);
755            }
756            // QuickJS's `get_prop_string` takes only `JS_TAG_STRING`: a rope
757            // (`JS_TAG_STRING_ROPE`) reads as absent.
758            Some(
759                heap.get_slot(object, slot)
760                    .as_string()
761                    .filter(|id| heap.strings.get(id.0).flat().is_some())
762                    .map(|id| heap.string(id).repr().units().collect()),
763            )
764        };
765        match read(object) {
766            Some(found) => found,
767            None => heap.prototype_of(object).and_then(read).flatten(),
768        }
769    }
770
771    /// A string as QuickJS's C-string conversion prints it: UTF-8 with lone
772    /// surrogates encoded as is (WTF-8), cut at the first NUL.
773    fn c_string(&mut self, units: &[u16]) {
774        let end = units
775            .iter()
776            .position(|&unit| unit == 0)
777            .unwrap_or(units.len());
778        wtf8(units[..end].iter().copied(), &mut self.out.0);
779    }
780
781    fn error(&mut self, object: ObjectId) {
782        let names = &self.cx.realm.names;
783        let (name, message, stack) = (names.name, names.message, names.stack);
784        match self.raw_string(object, name) {
785            Some(name) => self.c_string(&name),
786            None => self.out.push_str("Error"),
787        }
788        if let Some(message) = self.raw_string(object, message)
789            && message.first() != Some(&0)
790            && !message.is_empty()
791        {
792            self.out.push_str(": ");
793            self.c_string(&message);
794        }
795        if let Some(mut units) = self.raw_string(object, stack) {
796            if let Some(nul) = units.iter().position(|&unit| unit == 0) {
797                units.truncate(nul);
798            }
799            if units.last() == Some(&0x0a) {
800                units.pop();
801            }
802            self.out.push('\n');
803            wtf8(units.into_iter(), &mut self.out.0);
804        }
805    }
806
807    fn regexp(&mut self, object: ObjectId) {
808        let index = self.cx.heap.header(object).payload;
809        let record = self.cx.heap.regexps.get(index);
810        let pattern = self.units(record.source);
811        let flags = self.units(record.flags);
812        self.out.push('/');
813        if pattern.is_empty() {
814            self.out.push_str("(?:)");
815        } else {
816            let mut in_class = false;
817            let mut index = 0;
818            while index < pattern.len() {
819                let mut first = pattern[index];
820                index += 1;
821                let mut second = None;
822                match first {
823                    0x5c => {
824                        if let Some(&next) = pattern.get(index) {
825                            second = Some(next);
826                            index += 1;
827                        }
828                    }
829                    0x5d => in_class = false,
830                    0x5b if !in_class => {
831                        if pattern.get(index) == Some(&0x5d) {
832                            second = Some(0x5d);
833                            index += 1;
834                        }
835                        in_class = true;
836                    }
837                    0x0a => {
838                        first = 0x5c;
839                        second = Some(u16::from(b'n'));
840                    }
841                    0x0d => {
842                        first = 0x5c;
843                        second = Some(u16::from(b'r'));
844                    }
845                    0x2f if !in_class => {
846                        first = 0x5c;
847                        second = Some(0x2f);
848                    }
849                    _ => {}
850                }
851                // The pinned printer narrows each unit to a byte.
852                self.out.push(first as u8 as char);
853                if let Some(second) = second {
854                    self.out.push(second as u8 as char);
855                }
856            }
857        }
858        self.out.push('/');
859        // QuickJS prints `lre_get_flags` bits 0 to 7 as `gimsuydv`: bit 7 is
860        // in fact `LRE_FLAG_NAMED_GROUPS`, so a pattern with named groups
861        // shows `v` and the `v` flag (bit 8) never shows.
862        for flag in "gimsuyd".chars() {
863            if flags.contains(&(flag as u16)) {
864                self.out.push(flag);
865            }
866        }
867        if self.cx.heap.regexps.program(index).has_named_captures() {
868            self.out.push('v');
869        }
870    }
871}