// Copyright 2018-2026 the Deno authors. MIT license.

//! Value inspection engine ported from `01_console.js` (`formatValue`,
//! `formatRaw` and friends). The goal is byte-for-byte identical output with
//! the JavaScript implementation it replaces.

#![allow(clippy::all, reason = "ported Rust code with pre-existing lints")]

use std::collections::HashMap;

use deno_core::v8;

use super::quote;
use super::width::get_string_width;

/// A caught JavaScript exception, propagated like JS exceptions are: most
/// callers re-raise, `format_raw` converts to `[Internal Formatting Error]`.
pub struct JsErr(pub v8::Global<v8::Value>);

pub type R<T> = Result<T, JsErr>;

pub const K_OBJECT_TYPE: u8 = 0;
pub const K_ARRAY_TYPE: u8 = 1;
pub const K_ARRAY_EXTRAS_TYPE: u8 = 2;

const K_MIN_LINE_LENGTH: usize = 16;

// Iterator-state constants from 01_console.js.
const K_WEAK: u8 = 0;
const K_ITERATOR: u8 = 1;
const K_MAP_ENTRIES: u8 = 2;

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Compact {
  Bool(bool),
  Num(f64),
}

impl Compact {
  pub fn is_true(self) -> bool {
    matches!(self, Compact::Bool(true))
  }
  pub fn as_number(self) -> Option<f64> {
    match self {
      Compact::Num(n) => Some(n),
      Compact::Bool(_) => None,
    }
  }
}

#[derive(Clone, Copy)]
pub enum Sorted<'s> {
  No,
  Yes,
  Comparator(v8::Local<'s, v8::Function>),
}

#[derive(Clone, Copy, PartialEq)]
pub enum Getters {
  No,
  Yes,
  Get,
  Set,
}

#[derive(Clone, Copy)]
pub enum StylizeKind<'s> {
  NoColor,
  /// Style/color lookup tables (the `styles`/`colors` objects); honoring
  /// runtime mutation of e.g. `util.inspect.styles`.
  Theme {
    styles: v8::Local<'s, v8::Object>,
    colors: v8::Local<'s, v8::Object>,
    /// The original JS function, when one exists, so it can be handed to
    /// user custom-inspect hooks unchanged.
    js_fn: Option<v8::Local<'s, v8::Function>>,
  },
  Js(v8::Local<'s, v8::Function>),
}

/// Intrinsic JS values the engine needs, captured by the JS shim at module
/// initialization (so they are primordial-safe) and passed per call. Fields
/// are opened from the cached Globals lazily -- most calls (primitives) need
/// none of them.
pub struct Intrinsics<'c> {
  cached: &'c CachedIntrinsics,
}

macro_rules! intr_fn {
  ($name:ident) => {
    pub fn $name<'s>(
      &self,
      scope: &mut v8::PinScope<'s, '_>,
    ) -> v8::Local<'s, v8::Function> {
      v8::Local::new(scope, &self.cached.$name)
    }
  };
}
macro_rules! intr_obj {
  ($name:ident) => {
    pub fn $name<'s>(
      &self,
      scope: &mut v8::PinScope<'s, '_>,
    ) -> v8::Local<'s, v8::Object> {
      v8::Local::new(scope, &self.cached.$name)
    }
  };
}

impl<'c> Intrinsics<'c> {
  pub fn new(cached: &'c CachedIntrinsics) -> Self {
    Intrinsics { cached }
  }

  intr_fn!(function_to_string);
  intr_fn!(inspect_fn);
  intr_fn!(stylize_no_color);
  intr_fn!(create_stylize_with_color);
  intr_fn!(get_url_prototype);
  intr_fn!(get_cwd);
  intr_fn!(make_cross_context_stylize);
  intr_fn!(reg_exp_to_string);
  intr_fn!(number_value_of);
  intr_fn!(string_value_of);
  intr_fn!(boolean_value_of);
  intr_fn!(big_int_value_of);
  intr_fn!(symbol_value_of);
  intr_obj!(styles_obj);
  intr_obj!(colors_obj);
  intr_obj!(object_prototype);
  intr_obj!(error_prototype);

  /// `[prototype, name, constructor]` triples (`wellKnownPrototypes`).
  pub fn well_known<'s>(
    &self,
    scope: &mut v8::PinScope<'s, '_>,
  ) -> Vec<(
    v8::Local<'s, v8::Object>,
    &'c str,
    v8::Local<'s, v8::Function>,
  )> {
    self
      .cached
      .well_known
      .iter()
      .map(|(proto, name, ctor)| {
        (
          v8::Local::new(scope, proto),
          name.as_str(),
          v8::Local::new(scope, ctor),
        )
      })
      .collect()
  }
}

/// Pre-parsed intrinsics as v8 Globals, cached across calls (the intrinsics
/// object is created once by the JS shim and never changes).
pub struct CachedIntrinsics {
  pub key: v8::Global<v8::Object>,
  pub function_to_string: v8::Global<v8::Function>,
  pub inspect_fn: v8::Global<v8::Function>,
  pub stylize_no_color: v8::Global<v8::Function>,
  pub create_stylize_with_color: v8::Global<v8::Function>,
  pub styles_obj: v8::Global<v8::Object>,
  pub colors_obj: v8::Global<v8::Object>,
  pub object_prototype: v8::Global<v8::Object>,
  pub error_prototype: v8::Global<v8::Object>,
  pub well_known:
    Vec<(v8::Global<v8::Object>, String, v8::Global<v8::Function>)>,
  pub get_url_prototype: v8::Global<v8::Function>,
  pub get_cwd: v8::Global<v8::Function>,
  pub make_cross_context_stylize: v8::Global<v8::Function>,
  pub reg_exp_to_string: v8::Global<v8::Function>,
  pub number_value_of: v8::Global<v8::Function>,
  pub string_value_of: v8::Global<v8::Function>,
  pub boolean_value_of: v8::Global<v8::Function>,
  pub big_int_value_of: v8::Global<v8::Function>,
  pub symbol_value_of: v8::Global<v8::Function>,
}

impl CachedIntrinsics {
  pub fn matches<'s>(
    &self,
    scope: &mut v8::PinScope<'s, '_>,
    obj: v8::Local<'s, v8::Object>,
  ) -> bool {
    let key = v8::Local::new(scope, &self.key);
    let key_val: v8::Local<v8::Value> = key.into();
    key_val.strict_equals(obj.into())
  }
}

pub struct Ctx<'s> {
  pub show_hidden: bool,
  /// `None` == JS `null` (unlimited).
  pub depth: Option<f64>,
  pub colors: bool,
  pub custom_inspect: bool,
  pub show_proxy: bool,
  pub max_array_length: f64,
  pub max_string_length: f64,
  pub break_length: f64,
  pub escape_sequences: bool,
  pub compact: Compact,
  pub sorted: Sorted<'s>,
  pub getters: Getters,
  pub quotes: Vec<String>,
  pub iterable_limit: f64,
  pub trailing_comma: bool,
  pub indent_level: f64,
  pub str_abbreviate_size: Option<f64>,

  // Mutable state.
  pub indentation_lvl: usize,
  pub current_depth: f64,
  pub seen: Vec<v8::Local<'s, v8::Value>>,
  /// Insertion-ordered circular-reference map (`ctx.circular`).
  pub circular: Vec<(v8::Local<'s, v8::Value>, usize)>,
  pub circular_set: bool,
  pub budget: HashMap<usize, usize>,
  pub stylize: StylizeKind<'s>,

  /// `ctx.userOptions` (node compat).
  pub user_options: Option<v8::Local<'s, v8::Object>>,
  /// `ctx.numericSeparator` passthrough for `getUserOptions`.
  pub numeric_separator: Option<v8::Local<'s, v8::Value>>,
  /// `ctx.inspect` -- used to filter out util.inspect itself when probing
  /// the node custom-inspect symbol.
  pub ctx_inspect_fn: Option<v8::Local<'s, v8::Value>>,

  /// Memoized `getURLPrototype()` result.
  pub url_prototype: Option<Option<v8::Local<'s, v8::Object>>>,
  /// Memoized canonical circular-JSON error message (`%j` handling).
  pub circular_error_message: Option<String>,
  /// Memoized JS-function form of `stylize` for user hooks.
  pub stylize_js_fn: Option<v8::Local<'s, v8::Function>>,
  /// Per-call memo of resolved theme escape codes, keyed by flavour.
  pub theme_memo: HashMap<&'static str, Option<(String, String)>>,
  /// Native stack-frame address captured at the outermost `format_value`
  /// call, used to bound native-stack recursion (see `stack_low`).
  pub stack_base: Option<usize>,
}

impl<'s> Ctx<'s> {
  pub fn stylize(
    &mut self,
    scope: &mut v8::PinScope<'s, '_>,
    s: &str,
    flavour: &'static str,
  ) -> R<String> {
    if let StylizeKind::Theme { styles, colors, .. } = self.stylize {
      // The styles/colors tables are stable for the duration of a single
      // inspect call; memoize the resolved escape codes per flavour.
      if let Some(memo) = self.theme_memo.get(flavour) {
        return Ok(match memo {
          Some((open, close)) => format!("{open}{s}{close}"),
          None => s.to_string(),
        });
      }
      let resolved = resolve_theme_codes(scope, styles, colors, flavour);
      let result = match &resolved {
        Some((open, close)) => format!("{open}{s}{close}"),
        None => s.to_string(),
      };
      self.theme_memo.insert(flavour, resolved);
      return Ok(result);
    }
    stylize_with(scope, &self.stylize, s, flavour)
  }
}

/// Read one entry (open/close code) of a `colors[style]` pair. A string entry
/// (used by the Node-ecosystem truecolor pattern,
/// `util.inspect.colors.foo = ["38;2;255;0;0", "39"]`) is emitted verbatim;
/// `number_value` on it would yield NaN and print `\x1b[NaNm`.
fn color_code<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  arr: v8::Local<'s, v8::Object>,
  i: u32,
) -> Option<String> {
  let v = arr.get_index(scope, i)?;
  if v.is_string() {
    Some(v.to_rust_string_lossy(scope))
  } else {
    Some(fmt_js_number(v.number_value(scope)?))
  }
}

/// Resolve `styles[flavour]` -> `colors[style]` -> escape codes.
fn resolve_theme_codes<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  styles: v8::Local<'s, v8::Object>,
  colors: v8::Local<'s, v8::Object>,
  flavour: &str,
) -> Option<(String, String)> {
  // `styles`/`colors` are user-mutable (and alias entries are getters), so the
  // gets / ToString / ToNumber below can run user code that throws. Run under
  // a TryCatch so a throwing hook is caught (and cleared on drop) instead of
  // leaving a pending exception on the isolate for later V8 calls to trip on.
  v8::tc_scope!(tc, scope);
  let flavour_key = v8_str(tc, flavour);
  let style = styles.get(tc, flavour_key.into())?;
  if style.is_undefined() {
    return None;
  }
  let style_key = style.to_string(tc)?;
  let color = colors.get(tc, style_key.into())?;
  if color.is_undefined() {
    return None;
  }
  let arr = v8::Local::<v8::Object>::try_from(color).ok()?;
  let open = color_code(tc, arr, 0)?;
  let close = color_code(tc, arr, 1)?;
  Some((format!("\u{1b}[{open}m"), format!("\u{1b}[{close}m")))
}

pub fn stylize_with<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  kind: &StylizeKind<'s>,
  s: &str,
  flavour: &str,
) -> R<String> {
  match kind {
    StylizeKind::NoColor => Ok(s.to_string()),
    StylizeKind::Theme { styles, colors, .. } => {
      // `styles`/`colors` are user-mutable, so resolving the codes can run
      // user code; on any failure (missing style, throwing getter/valueOf)
      // fall back to the unstyled string. `resolve_theme_codes` runs the
      // lookups under a TryCatch so a thrown exception doesn't linger.
      match resolve_theme_codes(scope, *styles, *colors, flavour) {
        Some((open, close)) => Ok(format!("{open}{s}{close}")),
        None => Ok(s.to_string()),
      }
    }
    StylizeKind::Js(f) => {
      let str_val = v8_str(scope, s);
      let flavour_val = v8_str(scope, flavour);
      let undef: v8::Local<v8::Value> = v8::undefined(scope).into();
      let ret =
        js_call(scope, *f, undef, &[str_val.into(), flavour_val.into()])?;
      Ok(to_rust_string(scope, ret))
    }
  }
}

fn fmt_js_number(n: f64) -> String {
  if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e21 {
    format!("{}", n as i64)
  } else {
    format!("{}", n)
  }
}

// ---------------------------------------------------------------------------
// small helpers

pub fn v8_str<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  s: &str,
) -> v8::Local<'s, v8::String> {
  v8::String::new(scope, s).unwrap()
}

/// Internalized string for a static property key: property lookups with an
/// internalized key hit the fast path (no per-access string-table migration
/// like external strings incur).
pub fn v8_static_key<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  s: &'static str,
) -> v8::Local<'s, v8::String> {
  v8::String::new_from_one_byte(
    scope,
    s.as_bytes(),
    v8::NewStringType::Internalized,
  )
  .unwrap_or_else(|| v8_str(scope, s))
}

pub fn to_rust_string<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
) -> String {
  value.to_rust_string_lossy(scope)
}

pub fn grab_err<'s>(
  tc: &mut v8::PinScope<'s, '_>,
  exception: Option<v8::Local<'s, v8::Value>>,
) -> JsErr {
  let exc = exception.unwrap_or_else(|| v8::undefined(tc).into());
  JsErr(v8::Global::new(tc, exc))
}

/// Build a `JsErr` wrapping a fresh `TypeError`, for cases the old JS handled
/// with a catchable throw rather than letting Rust panic.
pub fn type_err<'s>(scope: &mut v8::PinScope<'s, '_>, message: &str) -> JsErr {
  let msg = v8_str(scope, message);
  let exc = v8::Exception::type_error(scope, msg);
  JsErr(v8::Global::new(scope, exc))
}

/// Build a `JsErr` wrapping a fresh `RangeError`.
pub fn range_err<'s>(scope: &mut v8::PinScope<'s, '_>, message: &str) -> JsErr {
  let msg = v8_str(scope, message);
  let exc = v8::Exception::range_error(scope, msg);
  JsErr(v8::Global::new(scope, exc))
}

/// Max native stack (bytes) the inspect recursion may consume past its entry
/// point before bailing. The depth option normally bounds recursion, but
/// `depth: null`/`Infinity` (a common `util.inspect` pattern) leaves it
/// unbounded; a deep acyclic structure then recurses on the native Rust stack
/// and V8's per-call stack guard aborts the process (`Check failed:
/// IsOnCentralStack()`) once a native API is hit too deep. The old JS recursed
/// on the JS stack and threw a catchable `RangeError`. A fixed recursion count
/// can't work across build profiles (debug frames are far larger than
/// release), so bound the bytes consumed since entry -- under V8's budget
/// (~0.8MB observed before it aborts) with headroom left to construct the
/// error, while staying close enough not to truncate sooner than the old code
/// rendered.
const STACK_BUDGET: usize = 640 * 1024;

/// Approximate address of the current native stack frame. Stacks grow down, so
/// for two frames the deeper one has the smaller address; the difference is the
/// stack consumed between them. Used only for relative measurement, so (unlike
/// `stacker::remaining_stack`, which needs the OS stack limit and can panic
/// when it can't be determined, e.g. under EMFILE) this never fails.
#[inline(never)]
fn stack_ptr() -> usize {
  let probe = 0u8;
  std::hint::black_box(&probe) as *const u8 as usize
}

/// True when the recursion has consumed more than `STACK_BUDGET` since entry.
fn stack_low(ctx: &Ctx<'_>) -> bool {
  match ctx.stack_base {
    Some(base) => base.saturating_sub(stack_ptr()) > STACK_BUDGET,
    None => false,
  }
}

/// Call a JS function, catching exceptions.
pub fn js_call<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  f: v8::Local<'s, v8::Function>,
  this: v8::Local<'s, v8::Value>,
  args: &[v8::Local<'s, v8::Value>],
) -> R<v8::Local<'s, v8::Value>> {
  v8::tc_scope!(tc, scope);
  match f.call(tc, this, args) {
    Some(v) => Ok(v),
    None => {
      let exc = tc.exception();
      Err(grab_err(tc, exc))
    }
  }
}

/// `obj[key]` with exceptions propagated as `Err`.
pub fn js_get<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  obj: v8::Local<'s, v8::Object>,
  key: v8::Local<'s, v8::Value>,
) -> R<v8::Local<'s, v8::Value>> {
  v8::tc_scope!(tc, scope);
  match obj.get(tc, key) {
    Some(v) => Ok(v),
    None => {
      let exc = tc.exception();
      Err(grab_err(tc, exc))
    }
  }
}

pub fn js_get_str<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  obj: v8::Local<'s, v8::Object>,
  key: &'static str,
) -> R<v8::Local<'s, v8::Value>> {
  let key = v8_static_key(scope, key);
  js_get(scope, obj, key.into())
}

/// `obj[key]`, swallowing exceptions (mirrors JS `try { ... } catch {}`).
pub fn try_get<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  obj: v8::Local<'s, v8::Object>,
  key: v8::Local<'s, v8::Value>,
) -> Option<v8::Local<'s, v8::Value>> {
  v8::tc_scope!(tc, scope);
  obj.get(tc, key)
}

pub fn try_get_str<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  obj: v8::Local<'s, v8::Object>,
  key: &'static str,
) -> Option<v8::Local<'s, v8::Value>> {
  let key = v8_static_key(scope, key);
  try_get(scope, obj, key.into())
}

/// `ObjectPrototypeIsPrototypeOf(proto, value)` -- walk the prototype chain.
pub fn is_prototype_of<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  proto: v8::Local<'s, v8::Value>,
  value: v8::Local<'s, v8::Value>,
) -> bool {
  let Ok(mut obj) = v8::Local::<v8::Object>::try_from(value) else {
    return false;
  };
  loop {
    let Some(p) = obj.get_prototype(scope) else {
      return false;
    };
    if p.is_null() {
      return false;
    }
    if p.strict_equals(proto) {
      return true;
    }
    let Ok(p_obj) = v8::Local::<v8::Object>::try_from(p) else {
      return false;
    };
    obj = p_obj;
  }
}

fn includes_identity<'s>(
  list: &[v8::Local<'s, v8::Value>],
  value: v8::Local<'s, v8::Value>,
) -> bool {
  list.iter().any(|v| v.strict_equals(value))
}

/// `isUndetectableObject`: typeof is "undefined" but value is not undefined
/// (document.all style). v8: value.is_undefined() false but TypeOf says
/// undefined.
fn is_undetectable<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
) -> bool {
  if value.is_undefined() {
    return false;
  }
  let type_of = value.type_of(scope).to_rust_string_lossy(scope);
  type_of == "undefined"
}

pub fn symbol_for<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  description: &str,
) -> v8::Local<'s, v8::Symbol> {
  let key = v8_str(scope, description);
  v8::Symbol::for_key(scope, key)
}

// ---------------------------------------------------------------------------
// formatValue

pub fn format_value<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
  typed_array: bool,
) -> R<String> {
  // Bound native-stack recursion (see STACK_BUDGET); throw a catchable
  // RangeError instead of aborting when `depth` is unlimited.
  if ctx.stack_base.is_none() {
    ctx.stack_base = Some(stack_ptr());
  }
  if stack_low(ctx) {
    return Err(range_err(scope, "Maximum call stack size exceeded"));
  }
  // Primitive types cannot have properties.
  if !value.is_object()
    && !value.is_function()
    && !is_undetectable(scope, value)
  {
    if value.is_null() {
      return ctx.stylize(scope, "null", "null");
    }
    return format_primitive(scope, ctx, value, false);
  }
  if value.is_null() {
    return ctx.stylize(scope, "null", "null");
  }

  // Memorize the context for custom inspection on proxies.
  let context = value;
  let mut value = value;
  let mut proxy_details: Option<(
    v8::Local<'s, v8::Value>,
    v8::Local<'s, v8::Value>,
  )> = None;
  if let Ok(proxy) = v8::Local::<v8::Proxy>::try_from(value) {
    let target = proxy.get_target(scope);
    let handler = proxy.get_handler(scope);
    // A revoked proxy has null target/handler. Proceeding would feed `null`
    // into `format_raw` (which expects an object) and abort the process; the
    // old JS threw a catchable TypeError instead.
    if target.is_null() || handler.is_null() {
      return Err(type_err(
        scope,
        "Cannot inspect a proxy that has been revoked",
      ));
    }
    if !ctx.show_proxy {
      // Inspect the underlying target directly. Unwrap nested proxies fully so
      // downstream type checks (array/typed-array/map/...) see through a proxy
      // chain, matching the old console's `ArrayIsArray`/prototype probes which
      // pierce proxies (`new Proxy(new Proxy([1, 2], {}), {})` -> `[ 1, 2 ]`).
      value = target;
      while let Ok(inner) = v8::Local::<v8::Proxy>::try_from(value) {
        let inner_target = inner.get_target(scope);
        if inner_target.is_null() || inner.get_handler(scope).is_null() {
          return Err(type_err(
            scope,
            "Cannot inspect a proxy that has been revoked",
          ));
        }
        value = inner_target;
      }
    } else {
      proxy_details = Some((target, handler));
    }
  }

  // Provide a hook for user-specified inspect functions.
  if ctx.custom_inspect {
    if let Some(result) = try_custom_inspect(
      scope,
      intr,
      ctx,
      context,
      value,
      proxy_details,
      recurse_times,
    )? {
      return Ok(result);
    }
  }

  // Circular reference detection.
  if includes_identity(&ctx.seen, value) {
    let mut index = 1;
    if !ctx.circular_set {
      ctx.circular_set = true;
      ctx.circular.push((value, index));
    } else {
      match ctx.circular.iter().find(|(v, _)| v.strict_equals(value)) {
        Some((_, i)) => index = *i,
        None => {
          index = ctx.circular.len() + 1;
          ctx.circular.push((value, index));
        }
      }
    }
    return ctx.stylize(scope, &format!("[Circular *{index}]"), "special");
  }

  format_raw(
    scope,
    intr,
    ctx,
    value,
    recurse_times,
    typed_array,
    proxy_details,
  )
}

/// Returns `Ok(Some(string))` when a custom-inspect hook produced output.
fn try_custom_inspect<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  context: v8::Local<'s, v8::Value>,
  value: v8::Local<'s, v8::Value>,
  proxy_details: Option<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Value>)>,
  recurse_times: f64,
) -> R<Option<String>> {
  let inspect_target = match proxy_details {
    Some((target, _)) => target,
    None => value,
  };
  let Ok(inspect_target_obj) =
    v8::Local::<v8::Object>::try_from(inspect_target)
  else {
    return Ok(None);
  };
  let Ok(value_obj) = v8::Local::<v8::Object>::try_from(value) else {
    return Ok(None);
  };

  let custom_inspect = symbol_for(scope, "Deno.customInspect");
  let private_custom_inspect = symbol_for(scope, "Deno.privateCustomInspect");

  for (sym, _private) in
    [(custom_inspect, false), (private_custom_inspect, true)]
  {
    // ReflectHas walks the prototype chain.
    let has = {
      v8::tc_scope!(tc, scope);
      inspect_target_obj.has(tc, sym.into()).unwrap_or(false)
    };
    if has {
      let func = js_get(scope, inspect_target_obj, sym.into())?;
      if let Ok(func) = v8::Local::<v8::Function>::try_from(func) {
        // return String(value[customInspect](inspect, ctx));
        let ctx_obj = materialize_ctx(scope, intr, ctx)?;
        let inspect_fn = intr.inspect_fn(scope);
        let ret =
          js_call(scope, func, value, &[inspect_fn.into(), ctx_obj.into()])?;
        // Read back state the hook may have changed through nested
        // `inspect(..., ctx)` calls (seen/circular are shared in JS).
        readback_ctx(scope, ctx, ctx_obj);
        let s = {
          v8::tc_scope!(tc, scope);
          match ret.to_string(tc) {
            Some(s) => s.to_rust_string_lossy(tc),
            None => {
              let exc = tc.exception();
              return Err(grab_err(tc, exc));
            }
          }
        };
        return Ok(Some(s));
      }
    }
  }

  // node custom inspect symbol.
  let node_sym = symbol_for(scope, "nodejs.util.inspect.custom");
  let maybe_custom = {
    v8::tc_scope!(tc, scope);
    inspect_target_obj.get(tc, node_sym.into())
  };
  let Some(maybe_custom) = maybe_custom else {
    return Ok(None);
  };
  let Ok(custom_fn) = v8::Local::<v8::Function>::try_from(maybe_custom) else {
    return Ok(None);
  };

  // Filter out the util module's own inspect function.
  if let Some(ctx_inspect) = ctx.ctx_inspect_fn {
    if maybe_custom.strict_equals(ctx_inspect) {
      return Ok(None);
    }
  }
  // Filter out prototype objects: value.constructor &&
  // value.constructor.prototype === value
  {
    let ctor = try_get_str(scope, value_obj, "constructor");
    if let Some(ctor) = ctor {
      if let Ok(ctor_obj) = v8::Local::<v8::Object>::try_from(ctor) {
        if ctor.is_object() || ctor.is_function() {
          if let Some(proto) = try_get_str(scope, ctor_obj, "prototype") {
            if proto.strict_equals(value) {
              return Ok(None);
            }
          }
        }
      }
    }
  }

  // depth = ctx.depth === null ? null : ctx.depth - recurseTimes
  let depth_val: v8::Local<v8::Value> = match ctx.depth {
    None => v8::null(scope).into(),
    Some(d) => v8::Number::new(scope, d - recurse_times).into(),
  };
  let object_prototype = intr.object_prototype(scope);
  let is_cross_context =
    !is_prototype_of(scope, object_prototype.into(), context);
  let user_options = get_user_options(scope, intr, ctx, is_cross_context)?;
  let ctx_inspect: v8::Local<v8::Value> = match ctx.ctx_inspect_fn {
    Some(f) => f,
    None => intr.inspect_fn(scope).into(),
  };
  let ret = js_call(
    scope,
    custom_fn,
    context,
    &[depth_val, user_options.into(), ctx_inspect],
  )?;

  // If the custom inspection method returned `this`, don't go into infinite
  // recursion.
  if ret.strict_equals(context) {
    return Ok(None);
  }
  if !ret.is_string() {
    return format_value(scope, intr, ctx, ret, recurse_times, false).map(Some);
  }
  let s = to_rust_string(scope, ret);
  let indent = " ".repeat(ctx.indentation_lvl);
  Ok(Some(s.replace('\n', &format!("\n{indent}"))))
}

/// `getUserOptions(ctx, isCrossContext)`.
fn get_user_options<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  is_cross_context: bool,
) -> R<v8::Local<'s, v8::Object>> {
  let ret = v8::Object::new(scope);
  let stylize_fn = ctx_stylize_js_fn(scope, intr, ctx)?;
  set_prop(scope, ret, "stylize", stylize_fn.into());
  set_bool(scope, ret, "showHidden", ctx.show_hidden);
  let depth: v8::Local<v8::Value> = match ctx.depth {
    None => v8::null(scope).into(),
    Some(d) => v8::Number::new(scope, d).into(),
  };
  set_prop(scope, ret, "depth", depth);
  set_bool(scope, ret, "colors", ctx.colors);
  set_bool(scope, ret, "customInspect", ctx.custom_inspect);
  set_bool(scope, ret, "showProxy", ctx.show_proxy);
  set_num(scope, ret, "maxArrayLength", ctx.max_array_length);
  set_num(scope, ret, "maxStringLength", ctx.max_string_length);
  set_num(scope, ret, "breakLength", ctx.break_length);
  let compact: v8::Local<v8::Value> = match ctx.compact {
    Compact::Bool(b) => v8::Boolean::new(scope, b).into(),
    Compact::Num(n) => v8::Number::new(scope, n).into(),
  };
  set_prop(scope, ret, "compact", compact);
  let sorted: v8::Local<v8::Value> = match ctx.sorted {
    Sorted::No => v8::Boolean::new(scope, false).into(),
    Sorted::Yes => v8::Boolean::new(scope, true).into(),
    Sorted::Comparator(f) => f.into(),
  };
  set_prop(scope, ret, "sorted", sorted);
  let getters: v8::Local<v8::Value> = match ctx.getters {
    Getters::No => v8::Boolean::new(scope, false).into(),
    Getters::Yes => v8::Boolean::new(scope, true).into(),
    Getters::Get => v8_str(scope, "get").into(),
    Getters::Set => v8_str(scope, "set").into(),
  };
  set_prop(scope, ret, "getters", getters);
  let numeric_separator: v8::Local<v8::Value> = match ctx.numeric_separator {
    Some(v) => v,
    None => v8::undefined(scope).into(),
  };
  set_prop(scope, ret, "numericSeparator", numeric_separator);

  // ...ctx.userOptions
  if let Some(user_options) = ctx.user_options {
    copy_enumerable_own_props(scope, user_options, ret)?;
  }

  if is_cross_context {
    // Remove the prototype and all non-primitive values; wrap stylize.
    let null_val: v8::Local<v8::Value> = v8::null(scope).into();
    ret.set_prototype(scope, null_val);
    let names = {
      v8::tc_scope!(tc, scope);
      ret.get_own_property_names(tc, Default::default())
    };
    if let Some(names) = names {
      for i in 0..names.length() {
        let Some(key) = names.get_index(scope, i) else {
          continue;
        };
        let Some(val) = try_get(scope, ret, key) else {
          continue;
        };
        if (val.is_object() || val.is_function()) && !val.is_null() {
          v8::tc_scope!(tc, scope);
          ret.delete(tc, key);
        }
      }
    }
    let make_cross_context_stylize = intr.make_cross_context_stylize(scope);
    let undef: v8::Local<v8::Value> = v8::undefined(scope).into();
    let wrapped = js_call(
      scope,
      make_cross_context_stylize,
      undef,
      &[stylize_fn.into()],
    )?;
    set_prop(scope, ret, "stylize", wrapped);
  }

  Ok(ret)
}

fn copy_enumerable_own_props<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  from: v8::Local<'s, v8::Object>,
  to: v8::Local<'s, v8::Object>,
) -> R<()> {
  let names = {
    v8::tc_scope!(tc, scope);
    from.get_own_property_names(
      tc,
      v8::GetPropertyNamesArgs {
        mode: v8::KeyCollectionMode::OwnOnly,
        property_filter: v8::PropertyFilter::ONLY_ENUMERABLE,
        index_filter: v8::IndexFilter::IncludeIndices,
        ..Default::default()
      },
    )
  };
  if let Some(names) = names {
    for i in 0..names.length() {
      let Some(key) = names.get_index(scope, i) else {
        continue;
      };
      let val = js_get(scope, from, key)?;
      set_prop_key(scope, to, key, val);
    }
  }
  Ok(())
}

pub fn set_prop<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  obj: v8::Local<'s, v8::Object>,
  key: &str,
  value: v8::Local<'s, v8::Value>,
) {
  let key = v8_str(scope, key);
  obj.set(scope, key.into(), value);
}

fn set_prop_key<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  obj: v8::Local<'s, v8::Object>,
  key: v8::Local<'s, v8::Value>,
  value: v8::Local<'s, v8::Value>,
) {
  obj.set(scope, key, value);
}

fn set_bool<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  obj: v8::Local<'s, v8::Object>,
  key: &str,
  value: bool,
) {
  let v = v8::Boolean::new(scope, value);
  set_prop(scope, obj, key, v.into());
}

fn set_num<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  obj: v8::Local<'s, v8::Object>,
  key: &str,
  value: f64,
) {
  let v = v8::Number::new(scope, value);
  set_prop(scope, obj, key, v.into());
}

/// The JS-function form of `ctx.stylize`, for handing to user hooks.
pub fn ctx_stylize_js_fn<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
) -> R<v8::Local<'s, v8::Function>> {
  if let Some(f) = ctx.stylize_js_fn {
    return Ok(f);
  }
  let f = match ctx.stylize {
    StylizeKind::Js(f) => f,
    StylizeKind::Theme { js_fn: Some(f), .. } => f,
    StylizeKind::Theme {
      styles,
      colors,
      js_fn: None,
    } => {
      let create_stylize_with_color = intr.create_stylize_with_color(scope);
      let undef: v8::Local<v8::Value> = v8::undefined(scope).into();
      let ret = js_call(
        scope,
        create_stylize_with_color,
        undef,
        &[styles.into(), colors.into()],
      )?;
      v8::Local::<v8::Function>::try_from(ret)
        .unwrap_or_else(|_| intr.stylize_no_color(scope))
    }
    StylizeKind::NoColor => intr.stylize_no_color(scope),
  };
  ctx.stylize_js_fn = Some(f);
  Ok(f)
}

/// Materialize the engine context as a JS ctx object compatible with the
/// historical `inspectOptions`/ctx shape, for `Deno.customInspect` hooks
/// that pass it back into `inspect(value, ctx)`.
pub fn materialize_ctx<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
) -> R<v8::Local<'s, v8::Object>> {
  let obj = get_user_options(scope, intr, ctx, false)?;
  set_num(scope, obj, "indentationLvl", ctx.indentation_lvl as f64);
  set_num(scope, obj, "currentDepth", ctx.current_depth);
  set_num(scope, obj, "indentLevel", ctx.indent_level);
  set_bool(scope, obj, "escapeSequences", ctx.escape_sequences);
  set_bool(scope, obj, "trailingComma", ctx.trailing_comma);
  set_num(scope, obj, "iterableLimit", ctx.iterable_limit);
  if let Some(n) = ctx.str_abbreviate_size {
    set_num(scope, obj, "strAbbreviateSize", n);
  }
  // quotes
  let quote_vals: Vec<v8::Local<v8::Value>> =
    ctx.quotes.iter().map(|q| v8_str(scope, q).into()).collect();
  let quotes = v8::Array::new_with_elements(scope, &quote_vals);
  set_prop(scope, obj, "quotes", quotes.into());
  // seen (shared identity matters for circular detection in nested calls)
  let seen_vals: Vec<v8::Local<v8::Value>> = ctx.seen.clone();
  let seen = v8::Array::new_with_elements(scope, &seen_vals);
  set_prop(scope, obj, "seen", seen.into());
  // budget
  let budget = v8::Object::new(scope);
  for (k, v) in &ctx.budget {
    let key = v8_str(scope, &k.to_string());
    let val = v8::Number::new(scope, *v as f64);
    budget.set(scope, key.into(), val.into());
  }
  set_prop(scope, obj, "budget", budget.into());
  let inspect_fn = intr.inspect_fn(scope);
  set_prop(scope, obj, "inspect", inspect_fn.into());
  Ok(obj)
}

/// Read back `seen` mutations after a custom-inspect hook ran (nested
/// inspect calls share the array in the JS implementation; we re-sync).
fn readback_ctx<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  ctx: &mut Ctx<'s>,
  ctx_obj: v8::Local<'s, v8::Object>,
) {
  if let Some(seen) = try_get_str(scope, ctx_obj, "seen") {
    if let Ok(arr) = v8::Local::<v8::Array>::try_from(seen) {
      let mut new_seen = Vec::with_capacity(arr.length() as usize);
      for i in 0..arr.length() {
        if let Some(v) = arr.get_index(scope, i) {
          new_seen.push(v);
        }
      }
      ctx.seen = new_seen;
    }
  }
}

// ---------------------------------------------------------------------------
// primitives

fn number_display<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
) -> String {
  let n = value.number_value(scope).unwrap_or(f64::NAN);
  if n == 0.0 && n.is_sign_negative() {
    "-0".to_string()
  } else {
    let num = v8::Number::new(scope, n);
    num.to_rust_string_lossy(scope)
  }
}

pub fn format_number<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
) -> R<String> {
  let s = number_display(scope, value);
  ctx.stylize(scope, &s, "number")
}

pub fn format_bigint<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
) -> R<String> {
  let s = value.to_rust_string_lossy(scope);
  ctx.stylize(scope, &format!("{s}n"), "bigint")
}

/// `maybeQuoteSymbol`.
fn maybe_quote_symbol<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  ctx: &Ctx<'s>,
  symbol: v8::Local<'s, v8::Symbol>,
) -> String {
  let desc = symbol.description(scope);
  if desc.is_undefined() {
    return symbol_to_string(scope, symbol);
  }
  let desc_str = desc.to_rust_string_lossy(scope);
  if quote::symbol_description_needs_no_quotes(&desc_str) {
    return symbol_to_string(scope, symbol);
  }
  format!(
    "Symbol({})",
    quote::quote_string(&desc_str, &ctx.quotes, ctx.escape_sequences)
  )
}

fn symbol_to_string<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  symbol: v8::Local<'s, v8::Symbol>,
) -> String {
  let desc = symbol.description(scope);
  if desc.is_undefined() {
    "Symbol()".to_string()
  } else {
    format!("Symbol({})", desc.to_rust_string_lossy(scope))
  }
}

/// `formatPrimitive(fn, value, ctx)`. `no_color` is used by `getBoxedBase`,
/// which always formats the inner primitive without color.
pub fn format_primitive<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  no_color: bool,
) -> R<String> {
  macro_rules! style {
    ($scope:expr, $text:expr, $flavour:expr) => {
      if no_color {
        Ok::<String, JsErr>($text.to_string())
      } else {
        ctx.stylize($scope, &$text, $flavour)
      }
    };
  }
  if value.is_string() {
    let mut s = value.to_rust_string_lossy(scope);
    let mut trailer = String::new();
    let utf16_len = v8::Local::<v8::String>::try_from(value)
      .map(|s| s.length())
      .unwrap_or(s.chars().count());
    if (utf16_len as f64) > ctx.max_string_length {
      let max = ctx.max_string_length as usize;
      let remaining = utf16_len - max;
      s = slice_utf16(&s, 0, max);
      trailer = format!(
        "... {} more character{}",
        remaining,
        if remaining > 1 { "s" } else { "" }
      );
    }
    let s_utf16_len = utf16_length(&s);
    if !ctx.compact.is_true()
      && s_utf16_len > K_MIN_LINE_LENGTH
      && (s_utf16_len as f64)
        > ctx.break_length - ctx.indentation_lvl as f64 - 4.0
    {
      // Split on lookahead-after-\n; each piece keeps its trailing \n.
      let mut parts: Vec<String> = Vec::new();
      let mut cur = String::new();
      for c in s.chars() {
        cur.push(c);
        if c == '\n' {
          parts.push(std::mem::take(&mut cur));
        }
      }
      if !cur.is_empty() || parts.is_empty() {
        parts.push(cur);
      }
      let mut out_parts = Vec::with_capacity(parts.len());
      for line in &parts {
        let quoted =
          quote::quote_string(line, &ctx.quotes, ctx.escape_sequences);
        out_parts.push(style!(scope, quoted, "string")?);
      }
      let joiner = format!(" +\n{}", " ".repeat(ctx.indentation_lvl + 2));
      return Ok(out_parts.join(&joiner) + &trailer);
    }
    let quoted = quote::quote_string(&s, &ctx.quotes, ctx.escape_sequences);
    return Ok(style!(scope, quoted, "string")? + &trailer);
  }
  if value.is_number() {
    let s = number_display(scope, value);
    return style!(scope, s, "number");
  }
  if value.is_big_int() {
    let s = format!("{}n", value.to_rust_string_lossy(scope));
    return style!(scope, s, "bigint");
  }
  if value.is_boolean() {
    let s = if value.is_true() { "true" } else { "false" };
    return style!(scope, s, "boolean");
  }
  if value.is_undefined() {
    return style!(scope, "undefined", "undefined");
  }
  // es6 symbol primitive
  let sym = v8::Local::<v8::Symbol>::try_from(value).unwrap();
  let s = maybe_quote_symbol(scope, ctx, sym);
  style!(scope, s, "symbol")
}

/// Slice a Rust string by UTF-16 code-unit indices (JS string semantics).
fn slice_utf16(s: &str, start: usize, end: usize) -> String {
  let mut out = String::new();
  let mut idx = 0usize;
  for c in s.chars() {
    let len = c.len_utf16();
    if idx >= end {
      break;
    }
    if idx >= start {
      // A surrogate pair straddling the boundary gets dropped whole; JS
      // would produce a lone surrogate, which can't be represented in a
      // Rust string. This only affects truncation mid-pair.
      if idx + len <= end {
        out.push(c);
      }
    }
    idx += len;
  }
  out
}

pub fn utf16_length(s: &str) -> usize {
  s.chars().map(|c| c.len_utf16()).sum()
}

// ---------------------------------------------------------------------------
// getPrefix / getCtxStyle

pub fn get_prefix(
  constructor: Option<&str>,
  tag: &str,
  fallback: &str,
  size: &str,
) -> String {
  match constructor {
    None => {
      if !tag.is_empty() && fallback != tag {
        format!("[{fallback}{size}: null prototype] [{tag}] ")
      } else {
        format!("[{fallback}{size}: null prototype] ")
      }
    }
    Some(constructor) => {
      if !tag.is_empty() && constructor != tag {
        format!("{constructor}{size} [{tag}] ")
      } else {
        format!("{constructor}{size} ")
      }
    }
  }
}

fn get_ctx_style<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
  constructor: Option<&str>,
  tag: &str,
) -> String {
  let mut fallback = String::new();
  if constructor.is_none() {
    fallback = v8::Local::<v8::Object>::try_from(value)
      .map(|o| o.get_constructor_name().to_rust_string_lossy(scope))
      .unwrap_or_default();
    if fallback == tag {
      fallback = "Object".to_string();
    }
  }
  get_prefix(constructor, tag, &fallback, "")
}

// ---------------------------------------------------------------------------
// keys

/// `getKeys(value, showHidden)`. Returns property keys (strings + symbols).
pub fn get_keys<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  value: v8::Local<'s, v8::Object>,
  show_hidden: bool,
) -> Vec<v8::Local<'s, v8::Value>> {
  let mut keys: Vec<v8::Local<'s, v8::Value>> = Vec::new();

  let is_module_ns =
    v8::Local::<v8::Value>::from(value).is_module_namespace_object();

  // Symbols first (collected separately, appended after names).
  let symbols: Vec<v8::Local<'s, v8::Value>> = {
    v8::tc_scope!(tc, scope);
    match value.get_property_names(
      tc,
      v8::GetPropertyNamesArgs {
        mode: v8::KeyCollectionMode::OwnOnly,
        property_filter: v8::PropertyFilter::SKIP_STRINGS,
        index_filter: v8::IndexFilter::IncludeIndices,
        key_conversion: v8::KeyConversionMode::KeepNumbers,
      },
    ) {
      Some(arr) => {
        let mut out = Vec::with_capacity(arr.length() as usize);
        for i in 0..arr.length() {
          if let Some(v) = arr.get_index(tc, i) {
            out.push(v);
          }
        }
        out
      }
      None => Vec::new(),
    }
  };

  if show_hidden {
    // ObjectGetOwnPropertyNames: all own string keys (incl. non-enumerable).
    v8::tc_scope!(tc, scope);
    if let Some(arr) = value.get_property_names(
      tc,
      v8::GetPropertyNamesArgs {
        mode: if is_module_ns {
          v8::KeyCollectionMode::IncludePrototypes
        } else {
          v8::KeyCollectionMode::OwnOnly
        },
        property_filter: v8::PropertyFilter::SKIP_SYMBOLS,
        index_filter: v8::IndexFilter::IncludeIndices,
        key_conversion: v8::KeyConversionMode::ConvertToString,
      },
    ) {
      for i in 0..arr.length() {
        if let Some(v) = arr.get_index(tc, i) {
          keys.push(v);
        }
      }
    }
    keys.extend(symbols.iter().copied());
  } else {
    // ObjectKeys: own enumerable string keys.
    //
    // For a module namespace object from an unevaluated module, enumerating
    // (which inspects each binding's descriptor) throws a ReferenceError on
    // uninitialized bindings. Node handles this by falling back to
    // ObjectGetOwnPropertyNames; mirror that by skipping the enumerable
    // filter for module namespaces (their string exports are all enumerable).
    {
      v8::tc_scope!(tc, scope);
      let mut property_filter = v8::PropertyFilter::SKIP_SYMBOLS;
      if !is_module_ns {
        property_filter = property_filter | v8::PropertyFilter::ONLY_ENUMERABLE;
      }
      if let Some(arr) = value.get_property_names(
        tc,
        v8::GetPropertyNamesArgs {
          mode: v8::KeyCollectionMode::OwnOnly,
          property_filter,
          index_filter: v8::IndexFilter::IncludeIndices,
          key_conversion: v8::KeyConversionMode::ConvertToString,
        },
      ) {
        for i in 0..arr.length() {
          if let Some(v) = arr.get_index(tc, i) {
            keys.push(v);
          }
        }
      }
    }
    // Filter symbols by enumerability.
    for sym in symbols {
      let enumerable = {
        v8::tc_scope!(tc, scope);
        let desc = v8::Local::<v8::Name>::try_from(sym)
          .ok()
          .and_then(|name| value.get_own_property_descriptor(tc, name));
        match desc {
          Some(desc) if desc.is_object() => {
            let desc_obj = desc.cast::<v8::Object>();
            try_get_str(tc, desc_obj, "enumerable")
              .map(|v| v.is_true())
              .unwrap_or(false)
          }
          _ => false,
        }
      };
      if enumerable {
        keys.push(sym);
      }
    }
  }

  // Errors hide the `cause` property.
  let error_prototype = intr.error_prototype(scope);
  if is_prototype_of(scope, error_prototype.into(), value.into()) {
    keys.retain(|key| {
      if !key.is_string() {
        return true;
      }
      key.to_rust_string_lossy(scope) != "cause"
    });
  }

  keys
}

/// `op_get_non_index_property_names(value, filter)` equivalent: own
/// non-index string property names. `only_enumerable` corresponds to the JS
/// callers' filter value 2 (ONLY_ENUMERABLE); 0 means all.
pub fn get_non_index_property_names<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  value: v8::Local<'s, v8::Object>,
  only_enumerable: bool,
) -> Vec<v8::Local<'s, v8::Value>> {
  let mut property_filter = v8::PropertyFilter::ALL_PROPERTIES;
  if only_enumerable {
    property_filter = property_filter | v8::PropertyFilter::ONLY_ENUMERABLE;
  }
  v8::tc_scope!(tc, scope);
  match value.get_property_names(
    tc,
    v8::GetPropertyNamesArgs {
      mode: v8::KeyCollectionMode::OwnOnly,
      property_filter,
      index_filter: v8::IndexFilter::SkipIndices,
      key_conversion: v8::KeyConversionMode::KeepNumbers,
    },
  ) {
    Some(arr) => {
      let mut out = Vec::with_capacity(arr.length() as usize);
      for i in 0..arr.length() {
        if let Some(v) = arr.get_index(tc, i) {
          out.push(v);
        }
      }
      out
    }
    None => Vec::new(),
  }
}

// ---------------------------------------------------------------------------
// constructor names / prototype properties

/// `builtInObjects`: own property names of the global object matching
/// `^[A-Z][a-zA-Z0-9]+$`. Resolved live like the JS module-level snapshot.
fn built_in_objects<'s>(scope: &mut v8::PinScope<'s, '_>) -> Vec<String> {
  let context = scope.get_current_context();
  let global = context.global(scope);
  let mut out = Vec::new();
  let names = {
    v8::tc_scope!(tc, scope);
    global.get_property_names(
      tc,
      v8::GetPropertyNamesArgs {
        mode: v8::KeyCollectionMode::OwnOnly,
        property_filter: v8::PropertyFilter::SKIP_SYMBOLS,
        index_filter: v8::IndexFilter::SkipIndices,
        key_conversion: v8::KeyConversionMode::ConvertToString,
      },
    )
  };
  if let Some(names) = names {
    for i in 0..names.length() {
      let Some(name) = names.get_index(scope, i) else {
        continue;
      };
      let name = name.to_rust_string_lossy(scope);
      let mut chars = name.chars();
      let first_ok = matches!(chars.next(), Some(c) if c.is_ascii_uppercase());
      if first_ok
        && name.chars().count() >= 2
        && chars.all(|c| c.is_ascii_alphanumeric())
      {
        out.push(name);
      }
    }
  }
  out
}

fn is_instanceof<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  proto: v8::Local<'s, v8::Value>,
  object: v8::Local<'s, v8::Value>,
) -> bool {
  // `isInstanceof` in JS is ObjectPrototypeIsPrototypeOf with a try/catch.
  if !proto.is_object() {
    return false;
  }
  is_prototype_of(scope, proto, object)
}

/// `addPrototypeProperties(ctx, main, obj, recurseTimes, output)`.
#[allow(clippy::too_many_arguments, reason = "formatting context")]
fn add_prototype_properties<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  main: v8::Local<'s, v8::Value>,
  obj_in: v8::Local<'s, v8::Value>,
  recurse_times: f64,
  output: &mut Vec<String>,
) -> R<()> {
  let builtins = built_in_objects(scope);
  let mut depth = 0;
  let mut obj = obj_in;
  let mut keys: Vec<v8::Local<'s, v8::Value>> = Vec::new();
  let mut key_set: Vec<String> = Vec::new();
  loop {
    if depth != 0 || main.strict_equals(obj) {
      let Ok(cur) = v8::Local::<v8::Object>::try_from(obj) else {
        return Ok(());
      };
      let proto = cur.get_prototype(scope);
      let Some(proto) = proto else {
        return Ok(());
      };
      if proto.is_null() {
        return Ok(());
      }
      obj = proto;
      let Ok(obj_o) = v8::Local::<v8::Object>::try_from(obj) else {
        return Ok(());
      };
      // Stop as soon as a built-in object type is detected.
      let descriptor = get_own_descriptor(scope, obj_o, "constructor");
      if let Some(desc) = descriptor {
        if let Some(value) = try_get_str(scope, desc, "value") {
          if value.is_function() {
            if let Ok(f) = v8::Local::<v8::Function>::try_from(value) {
              let name_key = v8_str(scope, "name");
              let name = {
                v8::tc_scope!(tc, scope);
                f.get(tc, name_key.into())
              };
              if let Some(name) = name {
                let name = name.to_rust_string_lossy(scope);
                if builtins.iter().any(|b| *b == name) {
                  return Ok(());
                }
              }
            }
          }
        }
      }
    }

    if depth == 0 {
      key_set.clear();
    } else {
      for key in &keys {
        key_set.push(key.to_rust_string_lossy(scope));
      }
    }
    let Ok(obj_o) = v8::Local::<v8::Object>::try_from(obj) else {
      return Ok(());
    };
    // ReflectOwnKeys(obj)
    keys = {
      v8::tc_scope!(tc, scope);
      match obj_o.get_property_names(
        tc,
        v8::GetPropertyNamesArgs {
          mode: v8::KeyCollectionMode::OwnOnly,
          property_filter: v8::PropertyFilter::ALL_PROPERTIES,
          index_filter: v8::IndexFilter::IncludeIndices,
          key_conversion: v8::KeyConversionMode::ConvertToString,
        },
      ) {
        Some(arr) => {
          let mut out = Vec::with_capacity(arr.length() as usize);
          for i in 0..arr.length() {
            if let Some(v) = arr.get_index(tc, i) {
              out.push(v);
            }
          }
          out
        }
        None => Vec::new(),
      }
    };
    ctx.seen.push(main);
    let main_obj = v8::Local::<v8::Object>::try_from(main).ok();
    for key in keys.clone() {
      // Ignore the `constructor` property and keys that exist on layers
      // above.
      let key_str = if key.is_string() {
        Some(key.to_rust_string_lossy(scope))
      } else {
        None
      };
      if key_str.as_deref() == Some("constructor") {
        continue;
      }
      if let Some(main_obj) = main_obj {
        let has_own = {
          v8::tc_scope!(tc, scope);
          v8::Local::<v8::Name>::try_from(key)
            .ok()
            .and_then(|name| main_obj.has_own_property(tc, name))
            .unwrap_or(false)
        };
        if has_own {
          continue;
        }
      }
      if depth != 0 {
        if let Some(s) = &key_str {
          if key_set.iter().any(|k| k == s) {
            continue;
          }
        }
      }
      let desc = {
        v8::tc_scope!(tc, scope);
        v8::Local::<v8::Name>::try_from(key)
          .ok()
          .and_then(|name| obj_o.get_own_property_descriptor(tc, name))
      };
      let Some(desc) = desc else {
        continue;
      };
      let Ok(desc) = v8::Local::<v8::Object>::try_from(desc) else {
        continue;
      };
      if let Some(value) = try_get_str(scope, desc, "value") {
        if value.is_function() {
          continue;
        }
      }
      let value = format_property(
        scope,
        intr,
        ctx,
        obj,
        recurse_times,
        key,
        K_OBJECT_TYPE,
        Some(desc),
        Some(main),
      )?;
      if ctx.colors {
        output.push(format!("\u{1b}[2m{value}\u{1b}[22m"));
      } else {
        output.push(value);
      }
    }
    ctx.seen.pop();
    depth += 1;
    if depth == 3 {
      break;
    }
  }
  Ok(())
}

fn get_own_descriptor<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  obj: v8::Local<'s, v8::Object>,
  key: &str,
) -> Option<v8::Local<'s, v8::Object>> {
  v8::tc_scope!(tc, scope);
  let key = v8_str(tc, key);
  let desc = obj.get_own_property_descriptor(tc, key.into())?;
  v8::Local::<v8::Object>::try_from(desc).ok()
}

/// `getConstructorName(obj, ctx, recurseTimes, protoProps)`. Returns `None`
/// for null-prototype objects.
fn get_constructor_name<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  obj_in: v8::Local<'s, v8::Value>,
  recurse_times: f64,
  proto_props: &mut Option<Vec<String>>,
) -> R<Option<String>> {
  let tmp = obj_in;
  let mut first_proto: Option<v8::Local<'s, v8::Value>> = None;
  let mut obj = Some(obj_in);

  while let Some(cur) = obj {
    if !(cur.is_object() || cur.is_function() || is_undetectable(scope, cur)) {
      break;
    }
    // Well-known prototype check.
    let well_known_list = intr.well_known(scope);
    let well_known = well_known_list
      .iter()
      .find(|(proto, _, _)| {
        let proto_val: v8::Local<v8::Value> = (*proto).into();
        proto_val.strict_equals(cur)
      })
      .map(|(_, name, ctor)| (name.to_string(), *ctor));
    if let Some((name, constructor)) = well_known {
      let is_instance = {
        v8::tc_scope!(tc, scope);
        tmp.instance_of(tc, constructor.into()).unwrap_or(false)
      };
      if is_instance {
        if proto_props.is_some()
          && first_proto.map(|p| !p.strict_equals(cur)).unwrap_or(true)
        {
          let start_obj = first_proto.unwrap_or(tmp);
          let mut output = std::mem::take(proto_props).unwrap_or_default();
          add_prototype_properties(
            scope,
            intr,
            ctx,
            tmp,
            start_obj,
            recurse_times,
            &mut output,
          )?;
          *proto_props = Some(output);
        }
        return Ok(Some(name));
      }
    }

    let cur_obj = v8::Local::<v8::Object>::try_from(cur).ok();
    let descriptor =
      cur_obj.and_then(|o| get_own_descriptor(scope, o, "constructor"));
    if let Some(desc) = descriptor {
      let desc_value = try_get_str(scope, desc, "value");
      if let Some(desc_value) = desc_value {
        if desc_value.is_function() {
          let f = v8::Local::<v8::Function>::try_from(desc_value).unwrap();
          let f_obj: v8::Local<v8::Object> = f.into();
          let name = try_get_str(scope, f_obj, "name")
            .map(|v| v.to_rust_string_lossy(scope))
            .unwrap_or_default();
          if !name.is_empty() {
            let proto = try_get_str(scope, f_obj, "prototype");
            let is_inst =
              proto.map(|p| is_instanceof(scope, p, tmp)).unwrap_or(false);
            if is_inst {
              if proto_props.is_some() {
                let first_differs =
                  first_proto.map(|p| !p.strict_equals(cur)).unwrap_or(true);
                let builtins = built_in_objects(scope);
                if first_differs || !builtins.iter().any(|b| *b == name) {
                  let start_obj = first_proto.unwrap_or(tmp);
                  let mut output =
                    std::mem::take(proto_props).unwrap_or_default();
                  add_prototype_properties(
                    scope,
                    intr,
                    ctx,
                    tmp,
                    start_obj,
                    recurse_times,
                    &mut output,
                  )?;
                  *proto_props = Some(output);
                }
              }
              return Ok(Some(name));
            }
          }
        }
      }
    }

    let proto = cur_obj.and_then(|o| o.get_prototype(scope));
    obj = match proto {
      Some(p) if !p.is_null() => Some(p),
      _ => None,
    };
    if first_proto.is_none() {
      // JS records `firstProto = obj` after the reassignment, including null.
      first_proto = Some(match proto {
        Some(p) => p,
        None => v8::null(scope).into(),
      });
    }
  }

  if let Some(fp) = first_proto {
    if fp.is_null() {
      return Ok(None);
    }
  } else {
    // While loop never ran (shouldn't happen for objects); treat like a
    // complex prototype below.
  }

  let res = v8::Local::<v8::Object>::try_from(tmp)
    .map(|o| o.get_constructor_name().to_rust_string_lossy(scope))
    .unwrap_or_default();

  if let Some(depth) = ctx.depth {
    if recurse_times > depth {
      return Ok(Some(format!("{res} <Complex prototype>")));
    }
  }

  let first_proto = first_proto.unwrap_or_else(|| v8::null(scope).into());
  let proto_constr = get_constructor_name(
    scope,
    intr,
    ctx,
    first_proto,
    recurse_times + 1.0,
    proto_props,
  )?;

  match proto_constr {
    None => {
      // `${res} <${inspect(firstProto, {...ctx, customInspect: false,
      // depth: -1})}>` -- `inspect()` re-applies its option post-processing:
      // iterableLimit overrides maxArrayLength, and colors re-derives the
      // default theme stylize.
      let mut sub_ctx = fork_ctx(ctx);
      sub_ctx.custom_inspect = false;
      sub_ctx.depth = Some(-1.0);
      sub_ctx.max_array_length = sub_ctx.iterable_limit;
      if let Some(n) = sub_ctx.str_abbreviate_size {
        sub_ctx.max_string_length = n;
      }
      if sub_ctx.colors {
        let styles = intr.styles_obj(scope);
        let colors = intr.colors_obj(scope);
        sub_ctx.stylize = StylizeKind::Theme {
          styles,
          colors,
          js_fn: None,
        };
        sub_ctx.stylize_js_fn = None;
      }
      let inner =
        format_value(scope, intr, &mut sub_ctx, first_proto, 0.0, false)?;
      merge_ctx_state(ctx, sub_ctx);
      Ok(Some(format!("{res} <{inner}>")))
    }
    Some(proto_constr) => Ok(Some(format!("{res} <{proto_constr}>"))),
  }
}

/// Clone the context for a nested `inspect()`-style call that shares
/// seen/circular/budget state.
fn fork_ctx<'s>(ctx: &Ctx<'s>) -> Ctx<'s> {
  Ctx {
    show_hidden: ctx.show_hidden,
    depth: ctx.depth,
    colors: ctx.colors,
    custom_inspect: ctx.custom_inspect,
    show_proxy: ctx.show_proxy,
    max_array_length: ctx.max_array_length,
    max_string_length: ctx.max_string_length,
    break_length: ctx.break_length,
    escape_sequences: ctx.escape_sequences,
    compact: ctx.compact,
    sorted: ctx.sorted,
    getters: ctx.getters,
    quotes: ctx.quotes.clone(),
    iterable_limit: ctx.iterable_limit,
    trailing_comma: ctx.trailing_comma,
    indent_level: ctx.indent_level,
    str_abbreviate_size: ctx.str_abbreviate_size,
    indentation_lvl: ctx.indentation_lvl,
    current_depth: ctx.current_depth,
    seen: ctx.seen.clone(),
    circular: ctx.circular.clone(),
    circular_set: ctx.circular_set,
    budget: ctx.budget.clone(),
    stylize: ctx.stylize,
    user_options: ctx.user_options,
    numeric_separator: ctx.numeric_separator,
    ctx_inspect_fn: ctx.ctx_inspect_fn,
    url_prototype: ctx.url_prototype,
    circular_error_message: ctx.circular_error_message.clone(),
    stylize_js_fn: ctx.stylize_js_fn,
    theme_memo: ctx.theme_memo.clone(),
    stack_base: ctx.stack_base,
  }
}

fn merge_ctx_state<'s>(ctx: &mut Ctx<'s>, sub: Ctx<'s>) {
  ctx.circular = sub.circular;
  ctx.circular_set = sub.circular_set;
  ctx.url_prototype = sub.url_prototype;
  ctx.circular_error_message = sub.circular_error_message;
}

// ---------------------------------------------------------------------------
// formatRaw

struct FormatterKind<'s> {
  /// What to run to produce the leading output entries.
  kind: FormatterId,
  bound_value: Option<v8::Local<'s, v8::Value>>,
  bound_size: usize,
  braces: (String, String),
}

#[derive(PartialEq, Clone, Copy)]
enum FormatterId {
  None,
  Array,
  Set,
  Map,
  TypedArray,
  MapIterator,
  SetIterator,
  Promise,
  WeakSet,
  WeakMap,
  WeakCollection,
  NamespaceObject,
  ArrayBuffer,
}

fn well_known_symbol_iterator<'s>(
  scope: &mut v8::PinScope<'s, '_>,
) -> v8::Local<'s, v8::Symbol> {
  v8::Symbol::get_iterator(scope)
}

#[allow(clippy::too_many_arguments, reason = "formatting context")]
fn format_raw<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
  typed_array_marker: bool,
  proxy_details: Option<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Value>)>,
) -> R<String> {
  let value_obj = v8::Local::<v8::Object>::try_from(value)
    .expect("format_raw requires an object");

  let mut proto_props: Option<Vec<String>> = None;
  let within_depth = match ctx.depth {
    None => true,
    Some(d) => recurse_times <= d,
  };
  if ctx.show_hidden && within_depth {
    proto_props = Some(Vec::new());
  }

  let constructor = get_constructor_name(
    scope,
    intr,
    ctx,
    value,
    recurse_times,
    &mut proto_props,
  )?;
  // Reset the variable to check for this later on.
  if let Some(props) = &proto_props {
    if props.is_empty() {
      proto_props = None;
    }
  }

  let mut tag = String::new();
  if proxy_details.is_none() {
    let tag_symbol = v8::Symbol::get_to_string_tag(scope);
    let tag_value = try_get(scope, value_obj, tag_symbol.into());
    if let Some(tag_value) = tag_value {
      if tag_value.is_string() {
        tag = tag_value.to_rust_string_lossy(scope);
      }
    }
  }

  let mut base = String::new();
  let mut formatter = FormatterKind {
    kind: FormatterId::None,
    bound_value: None,
    bound_size: 0,
    braces: ("{".to_string(), "}".to_string()),
  };
  let mut keys: Vec<v8::Local<'s, v8::Value>> = Vec::new();
  let mut extras_type = K_OBJECT_TYPE;
  let only_enumerable = !ctx.show_hidden;

  if proxy_details.is_some() && ctx.show_proxy {
    // `Proxy ` + formatValue(ctx, proxyDetails, recurseTimes)
    let (target, handler) = proxy_details.unwrap();
    let arr = v8::Array::new_with_elements(scope, &[target, handler]);
    let inner =
      format_value(scope, intr, ctx, arr.into(), recurse_times, false)?;
    return Ok(format!("Proxy {inner}"));
  }

  // Iterators and the rest are split to reduce checks.
  let mut no_iterator = true;
  let iterator_symbol = well_known_symbol_iterator(scope);
  let has_iterator = {
    v8::tc_scope!(tc, scope);
    value_obj.has(tc, iterator_symbol.into()).unwrap_or(false)
  };
  if has_iterator || constructor.is_none() {
    no_iterator = false;
    if value.is_array() {
      let arr = v8::Local::<v8::Array>::try_from(value).unwrap();
      let length = arr.length();
      let prefix = if constructor.as_deref() != Some("Array") || !tag.is_empty()
      {
        get_prefix(
          constructor.as_deref(),
          &tag,
          "Array",
          &format!("({length})"),
        )
      } else {
        String::new()
      };
      keys = get_non_index_property_names(scope, value_obj, only_enumerable);
      formatter.braces = (format!("{prefix}["), "]".to_string());
      if length == 0 && keys.is_empty() && proto_props.is_none() {
        return Ok(format!("{}]", formatter.braces.0));
      }
      extras_type = K_ARRAY_EXTRAS_TYPE;
      formatter.kind = FormatterId::Array;
    } else if value.is_set() {
      let set = v8::Local::<v8::Set>::try_from(value).unwrap();
      let size = set.size();
      let prefix =
        get_prefix(constructor.as_deref(), &tag, "Set", &format!("({size})"));
      keys = get_keys(scope, intr, value_obj, ctx.show_hidden);
      formatter.kind = FormatterId::Set;
      formatter.bound_value = Some(value);
      if size == 0 && keys.is_empty() && proto_props.is_none() {
        return Ok(format!("{prefix}{{}}"));
      }
      formatter.braces = (format!("{prefix}{{"), "}".to_string());
    } else if value.is_map() {
      let map = v8::Local::<v8::Map>::try_from(value).unwrap();
      let size = map.size();
      let prefix =
        get_prefix(constructor.as_deref(), &tag, "Map", &format!("({size})"));
      keys = get_keys(scope, intr, value_obj, ctx.show_hidden);
      formatter.kind = FormatterId::Map;
      formatter.bound_value = Some(value);
      if size == 0 && keys.is_empty() && proto_props.is_none() {
        return Ok(format!("{prefix}{{}}"));
      }
      formatter.braces = (format!("{prefix}{{"), "}".to_string());
    } else if value.is_typed_array() {
      let ta = v8::Local::<v8::TypedArray>::try_from(value).unwrap();
      keys = get_non_index_property_names(scope, value_obj, only_enumerable);
      let size = ta.length();
      let fallback = "";
      let prefix = get_prefix(
        constructor.as_deref(),
        &tag,
        fallback,
        &format!("({size})"),
      );
      formatter.braces = (format!("{prefix}["), "]".to_string());
      if size == 0 && keys.is_empty() && !ctx.show_hidden {
        return Ok(format!("{}]", formatter.braces.0));
      }
      formatter.kind = FormatterId::TypedArray;
      formatter.bound_value = Some(value);
      formatter.bound_size = size;
      extras_type = K_ARRAY_EXTRAS_TYPE;
    } else if value.is_map_iterator() {
      keys = get_keys(scope, intr, value_obj, ctx.show_hidden);
      formatter.braces = get_iterator_braces("Map", &tag);
      formatter.kind = FormatterId::MapIterator;
    } else if value.is_set_iterator() {
      keys = get_keys(scope, intr, value_obj, ctx.show_hidden);
      formatter.braces = get_iterator_braces("Set", &tag);
      formatter.kind = FormatterId::SetIterator;
    } else {
      no_iterator = true;
    }
  }
  if no_iterator {
    keys = get_keys(scope, intr, value_obj, ctx.show_hidden);
    formatter.braces = ("{".to_string(), "}".to_string());
    let constructor_str = constructor.as_deref();
    if constructor_str == Some("Object") {
      if value.is_arguments_object() {
        formatter.braces.0 = "[Arguments] {".to_string();
      } else if !tag.is_empty() {
        formatter.braces.0 =
          format!("{}{{", get_prefix(constructor_str, &tag, "Object", ""));
      }
      if keys.is_empty() && proto_props.is_none() {
        return Ok(format!("{}}}", formatter.braces.0));
      }
    } else if value.is_function() {
      base = get_function_base(scope, intr, value, constructor_str, &tag)?;
      if keys.is_empty() && proto_props.is_none() {
        return ctx.stylize(scope, &base, "special");
      }
    } else if value.is_reg_exp() {
      base = regexp_to_string(scope, intr, value)?;
      let prefix = get_prefix(constructor_str, &tag, "RegExp", "");
      if prefix != "RegExp " {
        base = format!("{prefix}{base}");
      }
      let past_depth = match ctx.depth {
        Some(d) => recurse_times > d,
        None => false,
      };
      if (keys.is_empty() && proto_props.is_none()) || past_depth {
        return ctx.stylize(scope, &base, "regexp");
      }
    } else if value.is_date() {
      let date = v8::Local::<v8::Date>::try_from(value).unwrap();
      let time = date.value_of();
      base = if time.is_nan() {
        date_to_string_invalid()
      } else {
        date_to_iso_string(time)
      };
      let prefix = get_prefix(constructor_str, &tag, "Date", "");
      if prefix != "Date " {
        base = format!("{prefix}{base}");
      }
      if keys.is_empty() && proto_props.is_none() {
        return ctx.stylize(scope, &base, "date");
      }
    } else if is_intl_locale(scope, value) {
      formatter.braces.0 =
        format!("{}{{", get_prefix(constructor_str, &tag, "Intl.Locale", ""));
      let extra_keys = [
        "baseName",
        "calendar",
        "caseFirst",
        "collation",
        "hourCycle",
        "language",
        "numberingSystem",
        "numeric",
        "region",
        "script",
      ];
      let mut new_keys: Vec<v8::Local<'s, v8::Value>> =
        extra_keys.iter().map(|k| v8_str(scope, k).into()).collect();
      new_keys.extend(keys);
      keys = new_keys;
    } else if is_temporal_object(scope, value) {
      // return ctx.stylize(value.toString(), "temporal");
      let s = {
        v8::tc_scope!(tc, scope);
        match value.to_string(tc) {
          Some(s) => s.to_rust_string_lossy(tc),
          None => {
            let exc = tc.exception();
            return Err(grab_err(tc, exc));
          }
        }
      };
      return ctx.stylize(scope, &s, "temporal");
    } else if value.is_native_error() || {
      let error_prototype = intr.error_prototype(scope);
      is_prototype_of(scope, error_prototype.into(), value)
    } {
      base = super::error_fmt::format_error(
        scope,
        intr,
        ctx,
        value_obj,
        constructor.as_deref(),
        &tag,
        &mut keys,
      )?;
      if keys.is_empty() && proto_props.is_none() {
        return Ok(base);
      }
    } else if value.is_array_buffer() || value.is_shared_array_buffer() {
      let array_type = if value.is_array_buffer() {
        "ArrayBuffer"
      } else {
        "SharedArrayBuffer"
      };
      let prefix = get_prefix(constructor_str, &tag, array_type, "");
      if !typed_array_marker {
        formatter.kind = FormatterId::ArrayBuffer;
        formatter.bound_value = Some(value);
      } else if keys.is_empty() && proto_props.is_none() {
        let byte_length = any_array_buffer_byte_length(scope, value);
        let num = v8::Number::new(scope, byte_length as f64);
        let formatted = format_number(scope, ctx, num.into())?;
        return Ok(format!("{prefix}{{ byteLength: {formatted} }}"));
      }
      formatter.braces.0 = format!("{prefix}{{");
      let mut new_keys: Vec<v8::Local<'s, v8::Value>> =
        vec![v8_str(scope, "byteLength").into()];
      new_keys.extend(keys);
      keys = new_keys;
    } else if value.is_data_view() {
      formatter.braces.0 =
        format!("{}{{", get_prefix(constructor_str, &tag, "DataView", ""));
      // .buffer goes last, it's not a primitive like the others.
      let mut new_keys: Vec<v8::Local<'s, v8::Value>> = vec![
        v8_str(scope, "byteLength").into(),
        v8_str(scope, "byteOffset").into(),
        v8_str(scope, "buffer").into(),
      ];
      new_keys.extend(keys);
      keys = new_keys;
    } else if value.is_promise() {
      formatter.braces.0 =
        format!("{}{{", get_prefix(constructor_str, &tag, "Promise", ""));
      formatter.kind = FormatterId::Promise;
      formatter.bound_value = Some(value);
    } else if value.is_weak_set() {
      formatter.braces.0 =
        format!("{}{{", get_prefix(constructor_str, &tag, "WeakSet", ""));
      formatter.kind = if ctx.show_hidden {
        FormatterId::WeakSet
      } else {
        FormatterId::WeakCollection
      };
      formatter.bound_value = Some(value);
    } else if value.is_weak_map() {
      formatter.braces.0 =
        format!("{}{{", get_prefix(constructor_str, &tag, "WeakMap", ""));
      formatter.kind = if ctx.show_hidden {
        FormatterId::WeakMap
      } else {
        FormatterId::WeakCollection
      };
      formatter.bound_value = Some(value);
    } else if value.is_module_namespace_object() {
      formatter.braces.0 =
        format!("{}{{", get_prefix(constructor_str, &tag, "Module", ""));
      formatter.kind = FormatterId::NamespaceObject;
      formatter.bound_value = Some(value);
    } else if is_boxed_primitive(value) {
      base = get_boxed_base(
        scope,
        intr,
        ctx,
        value,
        &mut keys,
        constructor_str,
        &tag,
      )?;
      if keys.is_empty() && proto_props.is_none() {
        return Ok(base);
      }
    } else if is_url_instance(scope, intr, ctx, value)?
      && !match ctx.depth {
        Some(d) => recurse_times > d,
        None => false,
      }
    {
      let href = js_get_str(scope, value_obj, "href")?;
      base = to_rust_string(scope, href);
      if keys.is_empty() && proto_props.is_none() {
        return Ok(base);
      }
    } else {
      if keys.is_empty() && proto_props.is_none() {
        return Ok(format!(
          "{}{{}}",
          get_ctx_style(scope, value, constructor_str, &tag)
        ));
      }
      formatter.braces.0 =
        format!("{}{{", get_ctx_style(scope, value, constructor_str, &tag));
    }
  }

  if let Some(d) = ctx.depth {
    if recurse_times > d {
      let style = get_ctx_style(scope, value, constructor.as_deref(), &tag);
      let mut constructor_name =
        style[..style.len().saturating_sub(1)].to_string();
      if constructor.is_some() {
        constructor_name = format!("[{constructor_name}]");
      }
      return ctx.stylize(scope, &constructor_name, "special");
    }
  }
  let recurse_times = recurse_times + 1.0;

  ctx.seen.push(value);
  ctx.current_depth = recurse_times;

  let mut braces = formatter.braces.clone();

  let output_result: R<Vec<String>> = (|| {
    let mut output = match formatter.kind {
      FormatterId::NamespaceObject => format_namespace_object(
        scope,
        intr,
        ctx,
        &mut keys,
        value,
        recurse_times,
      )?,
      FormatterId::MapIterator | FormatterId::SetIterator => {
        let (entries, is_key_value) = value_obj.preview_entries(scope);
        match entries {
          None => Vec::new(),
          Some(entries) => {
            if is_key_value {
              // Mark entry iterators as such.
              braces.0 = iterator_braces_mark_entries(&braces.0);
              format_map_iter_inner(
                scope,
                intr,
                ctx,
                recurse_times,
                entries,
                K_MAP_ENTRIES,
              )?
            } else {
              format_set_iter_inner(
                scope,
                intr,
                ctx,
                recurse_times,
                entries,
                K_ITERATOR,
              )?
            }
          }
        }
      }
      _ => run_formatter(scope, intr, ctx, &formatter, value, recurse_times)?,
    };
    for key in &keys {
      output.push(format_property(
        scope,
        intr,
        ctx,
        value,
        recurse_times,
        *key,
        extras_type,
        None,
        None,
      )?);
    }
    if let Some(props) = proto_props.take() {
      output.extend(props);
    }
    Ok(output)
  })();

  // Pop unconditionally now that all child values have been formatted: the
  // remaining logic (circular refs, user `stylize`, sorting) can still throw
  // via `?`, and leaving `value` in `seen` would cause false `[Circular]`
  // markers for it in sibling branches once an outer frame catches the error.
  ctx.seen.pop();

  let mut output = match output_result {
    Ok(output) => output,
    Err(err) => {
      let stack = error_stack_string(scope, &err);
      return ctx.stylize(
        scope,
        &format!("[Internal Formatting Error] {stack}"),
        "internalError",
      );
    }
  };
  if ctx.circular_set {
    let index = ctx
      .circular
      .iter()
      .find(|(v, _)| v.strict_equals(value))
      .map(|(_, i)| *i);
    if let Some(index) = index {
      let reference =
        ctx.stylize(scope, &format!("<ref *{index}>"), "special")?;
      if !ctx.compact.is_true() {
        base = if base.is_empty() {
          reference
        } else {
          format!("{reference} {base}")
        };
      } else {
        braces.0 = format!("{reference} {}", braces.0);
      }
    }
  }

  if !matches!(ctx.sorted, Sorted::No) {
    if extras_type == K_OBJECT_TYPE {
      sort_output(scope, ctx, &mut output)?;
    } else if keys.len() > 1 {
      let tail_start = output.len() - keys.len();
      let mut tail: Vec<String> = output[tail_start..].to_vec();
      sort_output(scope, ctx, &mut tail)?;
      output.splice(tail_start.., tail);
    }
  }

  let res = reduce_to_single_string(
    scope,
    ctx,
    output,
    &base,
    &braces,
    extras_type,
    recurse_times,
    Some(value),
  )?;
  let budget = ctx.budget.get(&ctx.indentation_lvl).copied().unwrap_or(0);
  let new_length = budget + res.len();
  ctx.budget.insert(ctx.indentation_lvl, new_length);
  if new_length > (1 << 27) {
    ctx.depth = Some(-1.0);
  }
  Ok(res)
}

fn sort_output<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  ctx: &mut Ctx<'s>,
  output: &mut [String],
) -> R<()> {
  match ctx.sorted {
    Sorted::No => {}
    Sorted::Yes => {
      output.sort_by(|a, b| {
        let a16: Vec<u16> = a.encode_utf16().collect();
        let b16: Vec<u16> = b.encode_utf16().collect();
        a16.cmp(&b16)
      });
    }
    Sorted::Comparator(f) => {
      // Manual insertion sort, calling the user comparator pairwise. Unlike
      // `slice::sort_by` (which since Rust 1.81 panics when it detects an
      // inconsistent total order), this tolerates arbitrary comparators the
      // way `Array.prototype.sort` does -- including a throwing one (which
      // makes the order inconsistent the moment it throws) and non-transitive
      // ones like `() => Math.random() - 0.5`.
      let mut items: Vec<String> = output.to_vec();
      let mut err = None;
      let mut i = 1;
      while i < items.len() {
        let mut j = i;
        while j > 0 {
          let a_v = v8_str(scope, &items[j - 1]);
          let b_v = v8_str(scope, &items[j]);
          let undef: v8::Local<v8::Value> = v8::undefined(scope).into();
          let n = match js_call(scope, f, undef, &[a_v.into(), b_v.into()]) {
            Ok(ret) => ret.number_value(scope).unwrap_or(f64::NAN),
            Err(e) => {
              err = Some(e);
              break;
            }
          };
          if n > 0.0 {
            items.swap(j - 1, j);
            j -= 1;
          } else {
            break;
          }
        }
        if err.is_some() {
          break;
        }
        i += 1;
      }
      if let Some(e) = err {
        return Err(e);
      }
      output.clone_from_slice(&items);
    }
  }
  Ok(())
}

pub fn error_stack_string<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  err: &JsErr,
) -> String {
  let exc = v8::Local::new(scope, &err.0);
  if let Ok(obj) = v8::Local::<v8::Object>::try_from(exc) {
    if let Some(stack) = try_get_str(scope, obj, "stack") {
      if !stack.is_undefined() {
        return stack.to_rust_string_lossy(scope);
      }
    }
  }
  exc.to_rust_string_lossy(scope)
}

fn get_iterator_braces(ty: &str, tag: &str) -> (String, String) {
  let expected = format!("{ty} Iterator");
  let mut tag = tag.to_string();
  if tag != expected {
    if !tag.is_empty() {
      tag.push_str("] [");
    }
    tag.push_str(&expected);
  }
  (format!("[{tag}] {{"), "}".to_string())
}

fn is_boxed_primitive(value: v8::Local<v8::Value>) -> bool {
  value.is_number_object()
    || value.is_string_object()
    || value.is_boolean_object()
    || value.is_big_int_object()
    || value.is_symbol_object()
}

fn is_intl_locale<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
) -> bool {
  let context = scope.get_current_context();
  let global = context.global(scope);
  let Some(intl) = try_get_str(scope, global, "Intl") else {
    return false;
  };
  let Ok(intl) = v8::Local::<v8::Object>::try_from(intl) else {
    return false;
  };
  let Some(locale) = try_get_str(scope, intl, "Locale") else {
    return false;
  };
  let Ok(locale) = v8::Local::<v8::Object>::try_from(locale) else {
    return false;
  };
  let Some(proto) = try_get_str(scope, locale, "prototype") else {
    return false;
  };
  is_prototype_of(scope, proto, value)
}

fn is_temporal_object<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
) -> bool {
  let context = scope.get_current_context();
  let global = context.global(scope);
  let Some(temporal) = try_get_str(scope, global, "Temporal") else {
    return false;
  };
  if temporal.is_undefined() {
    return false;
  }
  let Ok(temporal) = v8::Local::<v8::Object>::try_from(temporal) else {
    return false;
  };
  for name in [
    "Instant",
    "ZonedDateTime",
    "PlainDate",
    "PlainTime",
    "PlainDateTime",
    "PlainYearMonth",
    "PlainMonthDay",
    "Duration",
  ] {
    let Some(class) = try_get_str(scope, temporal, name) else {
      continue;
    };
    let Ok(class) = v8::Local::<v8::Object>::try_from(class) else {
      continue;
    };
    let Some(proto) = try_get_str(scope, class, "prototype") else {
      continue;
    };
    if is_prototype_of(scope, proto, value) {
      return true;
    }
  }
  false
}

fn is_url_instance<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
) -> R<bool> {
  let proto = match ctx.url_prototype {
    Some(memo) => memo,
    None => {
      let undef: v8::Local<v8::Value> = v8::undefined(scope).into();
      let ret = {
        let f = intr.get_url_prototype(scope);
        js_call(scope, f, undef, &[])
      }?;
      let proto = v8::Local::<v8::Object>::try_from(ret).ok();
      ctx.url_prototype = Some(proto);
      proto
    }
  };
  match proto {
    Some(proto) => Ok(is_prototype_of(scope, proto.into(), value)),
    None => Ok(false),
  }
}

fn any_array_buffer_byte_length<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  value: v8::Local<'s, v8::Value>,
) -> usize {
  let _ = scope;
  if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(value) {
    return ab.byte_length();
  }
  if let Ok(sab) = v8::Local::<v8::SharedArrayBuffer>::try_from(value) {
    return sab.byte_length();
  }
  0
}

fn regexp_to_string<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  regexp: v8::Local<'s, v8::Value>,
) -> R<String> {
  let ret = {
    let f = intr.reg_exp_to_string(scope);
    js_call(scope, f, regexp, &[])
  }?;
  Ok(to_rust_string(scope, ret))
}

// ---------------------------------------------------------------------------
// dates

/// Days-to-civil (Howard Hinnant's algorithm).
fn civil_from_days(z: i64) -> (i64, u32, u32) {
  let z = z + 719468;
  let era = if z >= 0 { z } else { z - 146096 } / 146097;
  let doe = (z - era * 146097) as u64; // [0, 146096]
  let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
  let y = yoe as i64 + era * 400;
  let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
  let mp = (5 * doy + 2) / 153; // [0, 11]
  let d = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31]
  let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12]
  (if m <= 2 { y + 1 } else { y }, m, d)
}

/// `Date.prototype.toISOString` for a finite epoch-milliseconds value.
pub fn date_to_iso_string(time: f64) -> String {
  let ms_total = time.floor() as i64;
  let days = ms_total.div_euclid(86_400_000);
  let ms_of_day = ms_total.rem_euclid(86_400_000);
  let (year, month, day) = civil_from_days(days);
  let hours = ms_of_day / 3_600_000;
  let minutes = (ms_of_day % 3_600_000) / 60_000;
  let seconds = (ms_of_day % 60_000) / 1000;
  let ms = ms_of_day % 1000;
  let year_str = if (0..=9999).contains(&year) {
    format!("{year:04}")
  } else if year < 0 {
    format!("-{:06}", -year)
  } else {
    format!("+{year:06}")
  };
  format!(
    "{year_str}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{ms:03}Z"
  )
}

pub fn date_to_string_invalid() -> String {
  "Invalid Date".to_string()
}

// ---------------------------------------------------------------------------
// function/class/boxed bases

fn get_class_base<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  value: v8::Local<'s, v8::Value>,
  constructor: Option<&str>,
  tag: &str,
) -> R<String> {
  let value_obj = v8::Local::<v8::Object>::try_from(value).unwrap();
  let has_name = {
    v8::tc_scope!(tc, scope);
    let key = v8_str(tc, "name");
    value_obj.has_own_property(tc, key.into()).unwrap_or(false)
  };
  let name_val = if has_name {
    js_get_str(scope, value_obj, "name")?
  } else {
    v8::undefined(scope).into()
  };
  let name_truthy = has_name && name_val.boolean_value(scope);
  let name = if name_truthy {
    name_val.to_rust_string_lossy(scope)
  } else {
    "(anonymous)".to_string()
  };
  let mut base = format!("class {name}");
  if let Some(ctor) = constructor {
    if ctor != "Function" {
      base.push_str(&format!(" [{ctor}]"));
    }
  }
  if !tag.is_empty() && constructor != Some(tag) {
    base.push_str(&format!(" [{tag}]"));
  }
  if constructor.is_some() {
    let proto = value_obj.get_prototype(scope);
    if let Some(proto) = proto {
      if let Ok(proto_obj) = v8::Local::<v8::Object>::try_from(proto) {
        let super_name = try_get_str(scope, proto_obj, "name")
          .filter(|v| v.is_string())
          .map(|v| v.to_rust_string_lossy(scope))
          .unwrap_or_default();
        if !super_name.is_empty() {
          base.push_str(&format!(" extends {super_name}"));
        }
      }
    }
  } else {
    base.push_str(" extends [null prototype]");
  }
  Ok(format!("[{base}]"))
}

/// Strip `//`-to-newline and `/* */` comments, replacing each with the
/// string "undefined" (a faithful port of a quirk in 01_console.js where
/// `RegExpPrototypeSymbolReplace` is called without a replacement value).
fn strip_comments_js_quirk(s: &str) -> String {
  let bytes = s.as_bytes();
  let mut out = String::with_capacity(s.len());
  let mut i = 0;
  while i < bytes.len() {
    if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
      // `//.*?\n` -- must find a newline; otherwise no match.
      if let Some(nl) = s[i..].find('\n') {
        out.push_str("undefined");
        i += nl + 1;
        continue;
      }
    }
    if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
      if let Some(end) = s[i + 2..].find("*/") {
        out.push_str("undefined");
        i += 2 + end + 2;
        continue;
      }
    }
    // Append the full UTF-8 char.
    let ch = s[i..].chars().next().unwrap();
    out.push(ch);
    i += ch.len_utf8();
  }
  out
}

/// `classRegExp` = `^(\s+[^(]*?)\s*{`: the text must start with whitespace
/// and contain no `(` before the first `{`.
fn matches_class_regexp(s: &str) -> bool {
  let mut chars = s.chars();
  match chars.next() {
    Some(c) if c.is_whitespace() => {}
    _ => return false,
  }
  for c in s.chars().skip(1) {
    match c {
      '{' => return true,
      '(' => return false,
      _ => {}
    }
  }
  false
}

fn get_function_base<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  value: v8::Local<'s, v8::Value>,
  constructor: Option<&str>,
  tag: &str,
) -> R<String> {
  let stringified_val = {
    let f = intr.function_to_string(scope);
    js_call(scope, f, value, &[])
  }?;
  let stringified = to_rust_string(scope, stringified_val);
  if stringified.starts_with("class") && stringified.ends_with('}') {
    let slice = &stringified[5..stringified.len() - 1];
    if let Some(bracket_index) = slice.find('{') {
      if !slice[..bracket_index].contains('(')
        || matches_class_regexp(&strip_comments_js_quirk(slice))
      {
        return get_class_base(scope, value, constructor, tag);
      }
    }
  }
  let mut ty = "Function".to_string();
  if value.is_generator_function() {
    ty = format!("Generator{ty}");
  }
  if value.is_async_function() {
    ty = format!("Async{ty}");
  }
  let mut base = format!("[{ty}");
  if constructor.is_none() {
    base.push_str(" (null prototype)");
  }
  let value_obj = v8::Local::<v8::Object>::try_from(value).unwrap();
  let name = js_get_str(scope, value_obj, "name")?;
  let name_str = if name.is_string() {
    name.to_rust_string_lossy(scope)
  } else {
    // `value.name === ""` is false for non-strings; the template coerces.
    name.to_rust_string_lossy(scope)
  };
  let name_is_empty_string = name.is_string() && name_str.is_empty();
  if name_is_empty_string {
    base.push_str(" (anonymous)");
  } else {
    base.push_str(&format!(": {name_str}"));
  }
  base.push(']');
  if let Some(ctor) = constructor {
    if ctor != ty {
      base.push_str(&format!(" {ctor}"));
    }
  }
  if !tag.is_empty() && constructor != Some(tag) {
    base.push_str(&format!(" [{tag}]"));
  }
  Ok(base)
}

fn get_boxed_base<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  keys: &mut Vec<v8::Local<'s, v8::Value>>,
  constructor: Option<&str>,
  tag: &str,
) -> R<String> {
  let (ty, primitive): (&str, v8::Local<'s, v8::Value>) =
    if value.is_number_object() {
      let n = {
        let f = intr.number_value_of(scope);
        js_call(scope, f, value, &[])
      }?;
      ("Number", n)
    } else if value.is_string_object() {
      let s = {
        let f = intr.string_value_of(scope);
        js_call(scope, f, value, &[])
      }?;
      // For boxed Strings remove the 0-n indexed entries.
      let len = v8::Local::<v8::String>::try_from(s)
        .map(|s| s.length())
        .unwrap_or(0);
      let drain = len.min(keys.len());
      keys.drain(0..drain);
      ("String", s)
    } else if value.is_boolean_object() {
      let b = {
        let f = intr.boolean_value_of(scope);
        js_call(scope, f, value, &[])
      }?;
      ("Boolean", b)
    } else if value.is_big_int_object() {
      let b = {
        let f = intr.big_int_value_of(scope);
        js_call(scope, f, value, &[])
      }?;
      ("BigInt", b)
    } else {
      let s = {
        let f = intr.symbol_value_of(scope);
        js_call(scope, f, value, &[])
      }?;
      ("Symbol", s)
    };

  let mut base = format!("[{ty}");
  if Some(ty) != constructor {
    match constructor {
      None => base.push_str(" (null prototype)"),
      Some(c) => base.push_str(&format!(" ({c})")),
    }
  }
  let inner = format_primitive(scope, ctx, primitive, true)?;
  base.push_str(&format!(": {inner}]"));
  if !tag.is_empty() && Some(tag) != constructor {
    base.push_str(&format!(" [{tag}]"));
  }
  if !keys.is_empty() || matches!(ctx.stylize, StylizeKind::NoColor) {
    return Ok(base);
  }
  let flavour: &'static str = match ty {
    "Number" => "number",
    "String" => "string",
    "Boolean" => "boolean",
    "BigInt" => "bigint",
    _ => "symbol",
  };
  ctx.stylize(scope, &base, flavour)
}

// ---------------------------------------------------------------------------
// collection formatters

fn run_formatter<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  formatter: &FormatterKind<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  match formatter.kind {
    FormatterId::None => Ok(Vec::new()),
    FormatterId::Array => format_array(scope, intr, ctx, value, recurse_times),
    FormatterId::Set => format_set(
      scope,
      intr,
      ctx,
      formatter.bound_value.unwrap(),
      recurse_times,
    ),
    FormatterId::Map => format_map(
      scope,
      intr,
      ctx,
      formatter.bound_value.unwrap(),
      recurse_times,
    ),
    FormatterId::TypedArray => format_typed_array(
      scope,
      intr,
      ctx,
      formatter.bound_value.unwrap(),
      formatter.bound_size,
      recurse_times,
    ),
    FormatterId::MapIterator | FormatterId::SetIterator => {
      unreachable!("iterators are special-cased before run_formatter")
    }
    FormatterId::Promise => format_promise(
      scope,
      intr,
      ctx,
      formatter.bound_value.unwrap(),
      recurse_times,
    ),
    FormatterId::WeakSet => format_weak_set(
      scope,
      intr,
      ctx,
      formatter.bound_value.unwrap(),
      recurse_times,
    ),
    FormatterId::WeakMap => format_weak_map(
      scope,
      intr,
      ctx,
      formatter.bound_value.unwrap(),
      recurse_times,
    ),
    FormatterId::WeakCollection => {
      Ok(vec![ctx.stylize(scope, "<items unknown>", "special")?])
    }
    FormatterId::NamespaceObject => {
      // keys are consumed by the namespace formatter in the caller.
      unreachable!("namespace objects are special-cased before run_formatter")
    }
    FormatterId::ArrayBuffer => {
      format_array_buffer(scope, ctx, formatter.bound_value.unwrap())
    }
  }
}

fn more_items(remaining: usize) -> String {
  format!(
    "... {} more item{}",
    remaining,
    if remaining > 1 { "s" } else { "" }
  )
}

fn format_array<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  let arr = v8::Local::<v8::Array>::try_from(value).unwrap();
  let value_obj: v8::Local<v8::Object> = arr.into();
  let val_len = arr.length() as usize;
  let len = (ctx.max_array_length.max(0.0) as usize).min(val_len);
  let remaining = val_len - len;
  let mut output = Vec::new();
  for i in 0..len {
    let has_own = {
      v8::tc_scope!(tc, scope);
      let key = v8_str(tc, &i.to_string());
      value_obj.has_own_property(tc, key.into()).unwrap_or(false)
    };
    if !has_own {
      return format_special_array(
        scope,
        intr,
        ctx,
        value,
        recurse_times,
        len,
        output,
        i,
      );
    }
    let key = v8::Number::new(scope, i as f64);
    output.push(format_property(
      scope,
      intr,
      ctx,
      value,
      recurse_times,
      key.into(),
      K_ARRAY_TYPE,
      None,
      None,
    )?);
  }
  if remaining > 0 {
    output.push(more_items(remaining));
  }
  Ok(output)
}

#[allow(clippy::too_many_arguments, reason = "formatting context")]
fn format_special_array<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
  max_length: usize,
  mut output: Vec<String>,
  start: usize,
) -> R<Vec<String>> {
  let arr = v8::Local::<v8::Array>::try_from(value).unwrap();
  let value_obj: v8::Local<v8::Object> = arr.into();
  // ObjectKeys(value)
  let keys: Vec<String> = {
    v8::tc_scope!(tc, scope);
    match value_obj.get_property_names(
      tc,
      v8::GetPropertyNamesArgs {
        mode: v8::KeyCollectionMode::OwnOnly,
        property_filter: v8::PropertyFilter::ONLY_ENUMERABLE
          | v8::PropertyFilter::SKIP_SYMBOLS,
        index_filter: v8::IndexFilter::IncludeIndices,
        key_conversion: v8::KeyConversionMode::ConvertToString,
      },
    ) {
      Some(names) => {
        let mut out = Vec::with_capacity(names.length() as usize);
        for i in 0..names.length() {
          if let Some(v) = names.get_index(tc, i) {
            out.push(v.to_rust_string_lossy(tc));
          }
        }
        out
      }
      None => Vec::new(),
    }
  };
  let mut index = start;
  let mut i = start;
  while i < keys.len() && output.len() < max_length {
    let key = &keys[i];
    let tmp: f64 = key.parse().unwrap_or(f64::NAN);
    // Arrays can only have up to 2^32 - 1 entries
    if tmp > (u32::MAX - 1) as f64 {
      break;
    }
    if index.to_string() != *key {
      if !quote::is_canonical_index(key) {
        break;
      }
      let empty_items = tmp as usize - index;
      let ending = if empty_items > 1 { "s" } else { "" };
      let message = format!("<{empty_items} empty item{ending}>");
      output.push(ctx.stylize(scope, &message, "undefined")?);
      index = tmp as usize;
      if output.len() == max_length {
        break;
      }
    }
    let key_v8 = v8_str(scope, key);
    output.push(format_property(
      scope,
      intr,
      ctx,
      value,
      recurse_times,
      key_v8.into(),
      K_ARRAY_TYPE,
      None,
      None,
    )?);
    index += 1;
    i += 1;
  }
  let remaining = arr.length() as i64 - index as i64;
  if output.len() != max_length {
    if remaining > 0 {
      let ending = if remaining > 1 { "s" } else { "" };
      let message = format!("<{remaining} empty item{ending}>");
      output.push(ctx.stylize(scope, &message, "undefined")?);
    }
  } else if remaining > 0 {
    output.push(more_items(remaining as usize));
  }
  Ok(output)
}

fn format_set<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  ctx.indentation_lvl += 2;
  let set = v8::Local::<v8::Set>::try_from(value).unwrap();
  let entries = set.as_array(scope);
  let val_len = set.size() as usize;
  let len = (ctx.iterable_limit.max(0.0) as usize).min(val_len);
  let remaining = val_len - len;
  let mut output = Vec::new();
  for i in 0..len {
    let Some(item) = entries.get_index(scope, i as u32) else {
      continue;
    };
    output.push(format_value(scope, intr, ctx, item, recurse_times, false)?);
  }
  if remaining > 0 {
    output.push(more_items(remaining));
  }
  ctx.indentation_lvl -= 2;
  Ok(output)
}

fn format_map<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  ctx.indentation_lvl += 2;
  let map = v8::Local::<v8::Map>::try_from(value).unwrap();
  let entries = map.as_array(scope); // [k0, v0, k1, v1, ...]
  let val_len = map.size() as usize;
  let len = (ctx.iterable_limit.max(0.0) as usize).min(val_len);
  let remaining = val_len - len;
  let mut output = Vec::new();
  for i in 0..len {
    let Some(k) = entries.get_index(scope, (i * 2) as u32) else {
      continue;
    };
    let Some(v) = entries.get_index(scope, (i * 2 + 1) as u32) else {
      continue;
    };
    let k_str = format_value(scope, intr, ctx, k, recurse_times, false)?;
    let v_str = format_value(scope, intr, ctx, v, recurse_times, false)?;
    output.push(format!("{k_str} => {v_str}"));
  }
  if remaining > 0 {
    output.push(more_items(remaining));
  }
  ctx.indentation_lvl -= 2;
  Ok(output)
}

fn format_typed_array<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  length: usize,
  recurse_times: f64,
) -> R<Vec<String>> {
  let value_obj = v8::Local::<v8::Object>::try_from(value).unwrap();
  let max_length = (ctx.max_array_length.max(0.0) as usize).min(length);
  let remaining = length - max_length;
  let mut output = Vec::with_capacity(max_length);
  let is_bigint_array =
    value.is_big_int64_array() || value.is_big_uint64_array();
  for i in 0..max_length {
    let Some(elem) = value_obj.get_index(scope, i as u32) else {
      continue;
    };
    let s = if is_bigint_array {
      format_bigint(scope, ctx, elem)?
    } else {
      format_number(scope, ctx, elem)?
    };
    output.push(s);
  }
  if remaining > 0 {
    output.push(more_items(remaining));
  }
  if ctx.show_hidden {
    // .buffer goes last, it's not a primitive like the others.
    ctx.indentation_lvl += 2;
    for key in [
      "BYTES_PER_ELEMENT",
      "length",
      "byteLength",
      "byteOffset",
      "buffer",
    ] {
      let v = js_get_str(scope, value_obj, key)?;
      let str_ = format_value(scope, intr, ctx, v, recurse_times, true)?;
      output.push(format!("[{key}]: {str_}"));
    }
    ctx.indentation_lvl -= 2;
  }
  Ok(output)
}

/// Marks entry iterators in the braces (mirrors formatIterator mutating
/// braces[0]).
fn iterator_braces_mark_entries(braces0: &str) -> String {
  if let Some(stripped) = braces0.strip_suffix(" Iterator] {") {
    format!("{stripped} Entries] {{")
  } else {
    braces0.to_string()
  }
}

fn format_promise<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
  match promise.state() {
    v8::PromiseState::Pending => {
      Ok(vec![ctx.stylize(scope, "<pending>", "special")?])
    }
    state => {
      let result = promise.result(scope);
      ctx.indentation_lvl += 2;
      let str_ = format_value(scope, intr, ctx, result, recurse_times, false)?;
      ctx.indentation_lvl -= 2;
      Ok(vec![if state == v8::PromiseState::Rejected {
        let rejected = ctx.stylize(scope, "<rejected>", "special")?;
        format!("{rejected} {str_}")
      } else {
        str_
      }])
    }
  }
}

fn format_weak_set<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  let value_obj = v8::Local::<v8::Object>::try_from(value).unwrap();
  let (entries, _) = value_obj.preview_entries(scope);
  let Some(entries) = entries else {
    return Ok(Vec::new());
  };
  format_set_iter_inner(scope, intr, ctx, recurse_times, entries, K_WEAK)
}

fn format_weak_map<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  let value_obj = v8::Local::<v8::Object>::try_from(value).unwrap();
  let (entries, _) = value_obj.preview_entries(scope);
  let Some(entries) = entries else {
    return Ok(Vec::new());
  };
  format_map_iter_inner(scope, intr, ctx, recurse_times, entries, K_WEAK)
}

fn format_map_iter_inner<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  recurse_times: f64,
  entries: v8::Local<'s, v8::Array>,
  state: u8,
) -> R<Vec<String>> {
  let max_array_length = ctx.max_array_length.max(0.0);
  // Entries exist as [key1, val1, key2, val2, ...]
  let len = entries.length() as usize / 2;
  let remaining = len as i64 - max_array_length as i64;
  let max_length = (max_array_length as usize).min(len);
  let mut output = Vec::with_capacity(max_length);
  ctx.indentation_lvl += 2;
  if state == K_WEAK {
    for i in 0..max_length {
      let pos = (i * 2) as u32;
      let k = entries.get_index(scope, pos).unwrap();
      let v = entries.get_index(scope, pos + 1).unwrap();
      let k_str = format_value(scope, intr, ctx, k, recurse_times, false)?;
      let v_str = format_value(scope, intr, ctx, v, recurse_times, false)?;
      output.push(format!("{k_str} => {v_str}"));
    }
    // Sort to have a halfway reliable output.
    if matches!(ctx.sorted, Sorted::No) {
      output.sort_by(|a, b| {
        let a16: Vec<u16> = a.encode_utf16().collect();
        let b16: Vec<u16> = b.encode_utf16().collect();
        a16.cmp(&b16)
      });
    }
  } else {
    for i in 0..max_length {
      let pos = (i * 2) as u32;
      let k = entries.get_index(scope, pos).unwrap();
      let v = entries.get_index(scope, pos + 1).unwrap();
      let res = vec![
        format_value(scope, intr, ctx, k, recurse_times, false)?,
        format_value(scope, intr, ctx, v, recurse_times, false)?,
      ];
      output.push(reduce_to_single_string(
        scope,
        ctx,
        res,
        "",
        &("[".to_string(), "]".to_string()),
        K_ARRAY_EXTRAS_TYPE,
        recurse_times,
        None,
      )?);
    }
  }
  ctx.indentation_lvl -= 2;
  if remaining > 0 {
    output.push(more_items(remaining as usize));
  }
  Ok(output)
}

fn format_set_iter_inner<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  recurse_times: f64,
  entries: v8::Local<'s, v8::Array>,
  state: u8,
) -> R<Vec<String>> {
  let max_array_length = ctx.max_array_length.max(0.0);
  let total = entries.length() as usize;
  let max_length = (max_array_length as usize).min(total);
  let mut output = Vec::with_capacity(max_length);
  ctx.indentation_lvl += 2;
  for i in 0..max_length {
    let v = entries.get_index(scope, i as u32).unwrap();
    output.push(format_value(scope, intr, ctx, v, recurse_times, false)?);
  }
  ctx.indentation_lvl -= 2;
  if state == K_WEAK && matches!(ctx.sorted, Sorted::No) {
    output.sort_by(|a, b| {
      let a16: Vec<u16> = a.encode_utf16().collect();
      let b16: Vec<u16> = b.encode_utf16().collect();
      a16.cmp(&b16)
    });
  }
  let remaining = total - max_length;
  if remaining > 0 {
    output.push(more_items(remaining));
  }
  Ok(output)
}

fn format_array_buffer<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
) -> R<Vec<String>> {
  let val_len = any_array_buffer_byte_length(scope, value);
  let len = (ctx.max_array_length.max(0.0) as usize).min(val_len);
  let bytes: Option<Vec<u8>> = if let Ok(ab) =
    v8::Local::<v8::ArrayBuffer>::try_from(value)
  {
    if ab.was_detached() {
      None
    } else {
      Some(read_buffer_bytes(ab.data(), len))
    }
  } else if let Ok(sab) = v8::Local::<v8::SharedArrayBuffer>::try_from(value) {
    let store = sab.get_backing_store();
    let data = store.data();
    Some(read_buffer_bytes(data, len))
  } else {
    None
  };
  let Some(bytes) = bytes else {
    return Ok(vec![ctx.stylize(scope, "(detached)", "special")?]);
  };
  let mut hex = String::with_capacity(len * 3);
  for (i, b) in bytes.iter().enumerate() {
    if i > 0 {
      hex.push(' ');
    }
    hex.push_str(&format!("{b:02x}"));
  }
  let mut str_ = hex;
  let remaining = val_len - len;
  if remaining > 0 {
    str_.push_str(&format!(
      " ... {} more byte{}",
      remaining,
      if remaining > 1 { "s" } else { "" }
    ));
  }
  let label = ctx.stylize(scope, "[Uint8Contents]", "special")?;
  Ok(vec![format!("{label}: <{str_}>")])
}

fn read_buffer_bytes(
  data: Option<std::ptr::NonNull<std::ffi::c_void>>,
  len: usize,
) -> Vec<u8> {
  match data {
    Some(ptr) if len > 0 => {
      // SAFETY: the buffer is alive (we hold a Local to it) and len is
      // clamped to its byte length.
      unsafe {
        std::slice::from_raw_parts(ptr.as_ptr() as *const u8, len).to_vec()
      }
    }
    _ => Vec::new(),
  }
}

fn format_namespace_object<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  keys: &mut Vec<v8::Local<'s, v8::Value>>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
) -> R<Vec<String>> {
  let mut output = Vec::with_capacity(keys.len());
  for key in keys.iter() {
    let result = format_property(
      scope,
      intr,
      ctx,
      value,
      recurse_times,
      *key,
      K_OBJECT_TYPE,
      None,
      None,
    );
    match result {
      Ok(s) => output.push(s),
      Err(_) => {
        // Use a stand-in object so indentation and line breaks stay
        // correct, then swap the value for `<uninitialized>`.
        let tmp = v8::Object::new(scope);
        let empty = v8_str(scope, "");
        tmp.set(scope, *key, empty.into());
        let formatted = format_property(
          scope,
          intr,
          ctx,
          tmp.into(),
          recurse_times,
          *key,
          K_OBJECT_TYPE,
          None,
          None,
        )?;
        let pos = formatted.rfind(' ').map(|p| p + 1).unwrap_or(0);
        let uninit = ctx.stylize(scope, "<uninitialized>", "special")?;
        output.push(format!("{}{}", &formatted[..pos], uninit));
      }
    }
  }
  // Reset the keys to an empty array. This prevents duplicated inspection.
  keys.clear();
  Ok(output)
}

// ---------------------------------------------------------------------------
// formatProperty

const COLOR_REGEXP_NOTE: () = ();

/// `removeColors(str)`: strip `\x1b\[\d\d?m` sequences.
pub fn remove_colors(s: &str) -> String {
  let _ = COLOR_REGEXP_NOTE;
  let chars: Vec<char> = s.chars().collect();
  let mut out = String::with_capacity(s.len());
  let mut i = 0;
  while i < chars.len() {
    if chars[i] == '\u{1b}'
      && i + 1 < chars.len()
      && chars[i + 1] == '['
      && i + 2 < chars.len()
      && chars[i + 2].is_ascii_digit()
    {
      let mut j = i + 3;
      let mut digits = 1;
      while j < chars.len() && chars[j].is_ascii_digit() && digits < 2 {
        j += 1;
        digits += 1;
      }
      if j < chars.len() && chars[j] == 'm' {
        i = j + 1;
        continue;
      }
    }
    out.push(chars[i]);
    i += 1;
  }
  out
}

#[allow(clippy::too_many_arguments, reason = "formatting context")]
fn format_property<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  intr: &Intrinsics<'_>,
  ctx: &mut Ctx<'s>,
  value: v8::Local<'s, v8::Value>,
  recurse_times: f64,
  key: v8::Local<'s, v8::Value>,
  ty: u8,
  desc_in: Option<v8::Local<'s, v8::Object>>,
  original: Option<v8::Local<'s, v8::Value>>,
) -> R<String> {
  let original = original.unwrap_or(value);
  let value_obj = v8::Local::<v8::Object>::try_from(value)
    .expect("format_property requires an object");
  let mut extra = " ".to_string();

  // desc = desc || ObjectGetOwnPropertyDescriptor(value, key) ||
  //   { value: value[key], enumerable: true }
  let mut desc_value: Option<v8::Local<'s, v8::Value>> = None;
  let mut desc_get: Option<v8::Local<'s, v8::Function>> = None;
  let mut desc_set_present = false;
  let mut enumerable = true;

  let desc_obj = match desc_in {
    Some(d) => Some(d),
    None => {
      // Note: own property descriptor; can throw for proxies, but those are
      // unwrapped before we get here. Errors propagate like in JS.
      v8::tc_scope!(tc, scope);
      match v8::Local::<v8::Name>::try_from(key) {
        Ok(name) => value_obj
          .get_own_property_descriptor(tc, name)
          .and_then(|d| v8::Local::<v8::Object>::try_from(d).ok()),
        Err(_) => {
          // Numeric key (array path): convert to string name.
          let key_str = key.to_rust_string_lossy(tc);
          let name = v8_str(tc, &key_str);
          value_obj
            .get_own_property_descriptor(tc, name.into())
            .and_then(|d| v8::Local::<v8::Object>::try_from(d).ok())
        }
      }
    }
  };

  match desc_obj {
    Some(desc) if !desc.is_undefined() => {
      let v = try_get_str(scope, desc, "value");
      if let Some(v) = v {
        if !v.is_undefined() {
          desc_value = Some(v);
        }
      }
      if desc_value.is_none() {
        if let Some(g) = try_get_str(scope, desc, "get") {
          if g.is_function() {
            desc_get = v8::Local::<v8::Function>::try_from(g).ok();
          }
        }
        if let Some(s) = try_get_str(scope, desc, "set") {
          desc_set_present = s.is_function();
        }
      }
      if let Some(e) = try_get_str(scope, desc, "enumerable") {
        enumerable = e.boolean_value(scope);
      }
    }
    _ => {
      // { value: value[key], enumerable: true }
      let v = js_get(scope, value_obj, key)?;
      if !v.is_undefined() {
        desc_value = Some(v);
      }
      enumerable = true;
    }
  }

  let str_;
  if let Some(v) = desc_value {
    let diff = if !ctx.compact.is_true() || ty != K_OBJECT_TYPE {
      2
    } else {
      3
    };
    ctx.indentation_lvl += diff;
    str_ = format_value(scope, intr, ctx, v, recurse_times, false)?;
    if diff == 3
      && (ctx.break_length as usize) < get_string_width(&str_, ctx.colors)
    {
      extra = format!("\n{}", " ".repeat(ctx.indentation_lvl));
    }
    ctx.indentation_lvl -= diff;
  } else if let Some(getter) = desc_get {
    let label = if desc_set_present {
      "Getter/Setter"
    } else {
      "Getter"
    };
    let should_eval = match ctx.getters {
      Getters::Yes => true,
      Getters::Get => !desc_set_present,
      Getters::Set => desc_set_present,
      Getters::No => false,
    };
    if should_eval {
      let result = js_call(scope, getter, original, &[]);
      match result {
        Ok(tmp) => {
          ctx.indentation_lvl += 2;
          if tmp.is_null() {
            let open = ctx.stylize(scope, &format!("[{label}:"), "special")?;
            let null_s = ctx.stylize(scope, "null", "null")?;
            let close = ctx.stylize(scope, "]", "special")?;
            str_ = format!("{open} {null_s}{close}");
          } else if tmp.is_object() {
            let lab = ctx.stylize(scope, &format!("[{label}]"), "special")?;
            let inner =
              format_value(scope, intr, ctx, tmp, recurse_times, false)?;
            str_ = format!("{lab} {inner}");
          } else {
            let primitive = format_primitive(scope, ctx, tmp, false)?;
            let open = ctx.stylize(scope, &format!("[{label}:"), "special")?;
            let close = ctx.stylize(scope, "]", "special")?;
            str_ = format!("{open} {primitive}{close}");
          }
          ctx.indentation_lvl -= 2;
        }
        Err(err) => {
          let message = {
            let exc = v8::Local::new(scope, &err.0);
            let msg = v8::Local::<v8::Object>::try_from(exc)
              .ok()
              .and_then(|o| try_get_str(scope, o, "message"))
              .map(|m| m.to_rust_string_lossy(scope))
              .unwrap_or_default();
            format!("<Inspection threw ({msg})>")
          };
          let open = ctx.stylize(scope, &format!("[{label}:"), "special")?;
          let close = ctx.stylize(scope, "]", "special")?;
          str_ = format!("{open} {message}{close}");
        }
      }
    } else {
      str_ = ctx.stylize(scope, &format!("[{label}]"), "special")?;
    }
  } else if desc_set_present {
    str_ = ctx.stylize(scope, "[Setter]", "special")?;
  } else {
    str_ = ctx.stylize(scope, "undefined", "undefined")?;
  }

  if ty == K_ARRAY_TYPE {
    return Ok(str_);
  }

  let name;
  if key.is_symbol() {
    let sym = v8::Local::<v8::Symbol>::try_from(key).unwrap();
    let tmp = quote::escape_meta_chars(&symbol_to_string(scope, sym));
    name = ctx.stylize(scope, &tmp, "symbol")?;
  } else {
    let key_str = key.to_rust_string_lossy(scope);
    if quote::is_identifier_like(&key_str) {
      name = if key_str == "__proto__" {
        "['__proto__']".to_string()
      } else {
        ctx.stylize(scope, &key_str, "name")?
      };
    } else {
      let quoted =
        quote::quote_string(&key_str, &ctx.quotes, ctx.escape_sequences);
      name = ctx.stylize(scope, &quoted, "string")?;
    }
  }

  let name = if !enumerable {
    format!("[{name}]")
  } else {
    name
  };
  Ok(format!("{name}:{extra}{str_}"))
}

// ---------------------------------------------------------------------------
// reduceToSingleString and helpers

fn is_below_break_length<'s>(
  ctx: &Ctx<'s>,
  output: &[String],
  start: usize,
  base: &str,
) -> bool {
  // Each entry is separated by at least a comma; start with a total length
  // of at least `output.length`.
  let mut total_length = output.len() + start;
  if (total_length + output.len()) as f64 > ctx.break_length {
    return false;
  }
  for item in output {
    if ctx.colors {
      total_length += utf16_length(&remove_colors(item));
    } else {
      total_length += utf16_length(item);
    }
    if total_length as f64 > ctx.break_length {
      return false;
    }
  }
  // Do not line up properties on the same line if `base` contains line
  // breaks.
  base.is_empty() || !base.contains('\n')
}

#[allow(clippy::too_many_arguments, reason = "formatting context")]
fn reduce_to_single_string<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  ctx: &mut Ctx<'s>,
  mut output: Vec<String>,
  base: &str,
  braces: &(String, String),
  extras_type: u8,
  recurse_times: f64,
  value: Option<v8::Local<'s, v8::Value>>,
) -> R<String> {
  if !ctx.compact.is_true() {
    if let Some(compact_num) = ctx.compact.as_number() {
      if compact_num >= 1.0 {
        // Memorize the original output length.
        let entries = output.len();
        // Group array elements together if the array contains at least six
        // separate entries.
        if extras_type == K_ARRAY_EXTRAS_TYPE && entries > 6 {
          output = group_array_elements(scope, ctx, output, value);
        }
        // Consolidate all entries of the local most inner depth up to
        // `ctx.compact`, as long as the properties are smaller than
        // `ctx.breakLength`.
        if ctx.current_depth - recurse_times < compact_num
          && entries == output.len()
        {
          let start = output.len()
            + ctx.indentation_lvl
            + utf16_length(&braces.0)
            + utf16_length(base)
            + 10;
          if is_below_break_length(ctx, &output, start, base) {
            let joined_output = output.join(", ");
            if !joined_output.contains('\n') {
              let base_part = if !base.is_empty() {
                format!("{base} ")
              } else {
                String::new()
              };
              return Ok(format!(
                "{base_part}{} {joined_output} {}",
                braces.0, braces.1
              ));
            }
          }
        }
      }
    }
    // Line up each entry on an individual line.
    let indentation = format!("\n{}", " ".repeat(ctx.indentation_lvl));
    let base_part = if !base.is_empty() {
      format!("{base} ")
    } else {
      String::new()
    };
    let trailing = if ctx.trailing_comma { "," } else { "" };
    return Ok(format!(
      "{base_part}{}{indentation}  {}{trailing}{indentation}{}",
      braces.0,
      output.join(&format!(",{indentation}  ")),
      braces.1
    ));
  }
  // Line up all entries on a single line in case the entries do not exceed
  // `breakLength`.
  if is_below_break_length(ctx, &output, 0, base) {
    let base_part = if !base.is_empty() {
      format!(" {base}")
    } else {
      String::new()
    };
    return Ok(format!(
      "{}{base_part} {} {}",
      braces.0,
      output.join(", "),
      braces.1
    ));
  }
  let indentation = " ".repeat(ctx.indentation_lvl);
  // If the opening "brace" is too large, like in the case of "Set {", force
  // the first item onto the next line.
  let ln = if base.is_empty() && utf16_length(&braces.0) == 1 {
    " ".to_string()
  } else {
    let base_part = if !base.is_empty() {
      format!(" {base}")
    } else {
      String::new()
    };
    format!("{base_part}\n{indentation}  ")
  };
  Ok(format!(
    "{}{ln}{} {}",
    braces.0,
    output.join(&format!(",\n{indentation}  ")),
    braces.1
  ))
}

/// JS `String.prototype.padStart`/`padEnd` with UTF-16 target lengths.
fn pad_utf16(s: &str, target: usize, pad_start: bool) -> String {
  let len = utf16_length(s);
  if len >= target {
    return s.to_string();
  }
  let padding = " ".repeat(target - len);
  if pad_start {
    format!("{padding}{s}")
  } else {
    format!("{s}{padding}")
  }
}

fn group_array_elements<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  ctx: &mut Ctx<'s>,
  output: Vec<String>,
  value: Option<v8::Local<'s, v8::Value>>,
) -> Vec<String> {
  let mut total_length = 0usize;
  let mut max_length = 0usize;
  let mut output_length = output.len();
  if (ctx.max_array_length as usize) < output.len() {
    // Exclude the "... n more items" part from the calculation.
    output_length -= 1;
  }
  let separator_space = 2; // one space + one separator
  let mut data_len = vec![0usize; output_length];
  for (i, item) in output.iter().take(output_length).enumerate() {
    let len = get_string_width(item, ctx.colors);
    data_len[i] = len;
    total_length += len + separator_space;
    if max_length < len {
      max_length = len;
    }
  }
  // Add two to `maxLength` for the space + comma between two entries.
  let actual_max = max_length + separator_space;
  if ((actual_max * 3 + ctx.indentation_lvl) as f64) < ctx.break_length
    && ((total_length as f64) / (actual_max as f64) > 5.0 || max_length <= 6)
  {
    let approx_char_heights = 2.5f64;
    let average_bias = ((actual_max as f64)
      - (total_length as f64) / (output.len() as f64))
      .sqrt();
    let biased_max = ((actual_max as f64) - 3.0 - average_bias).max(1.0);
    let compact_num = ctx.compact.as_number().unwrap_or(0.0);
    let columns_f =
      ((approx_char_heights * biased_max * (output_length as f64)).sqrt()
        / biased_max)
        .round();
    let columns = columns_f
      .min(
        ((ctx.break_length - ctx.indentation_lvl as f64) / (actual_max as f64))
          .floor(),
      )
      .min(compact_num * 4.0)
      .min(15.0);
    if columns <= 1.0 {
      return output;
    }
    let columns = columns as usize;
    let mut tmp = Vec::new();
    let mut max_line_length = Vec::with_capacity(columns);
    for i in 0..columns {
      let mut line_max_length = 0usize;
      let mut j = i;
      while j < output.len() {
        if data_len.get(j).copied().unwrap_or(0) > line_max_length {
          line_max_length = data_len[j];
        }
        j += columns;
      }
      max_line_length.push(line_max_length + separator_space);
    }
    // Pick alignment: numbers/bigints get right-aligned.
    let mut pad_start_order = true;
    if let Some(value) = value {
      if let Ok(value_obj) = v8::Local::<v8::Object>::try_from(value) {
        for i in 0..output.len() {
          let elem = value_obj.get_index(scope, i as u32);
          let is_numeric = elem
            .map(|e| e.is_number() || e.is_big_int())
            .unwrap_or(false);
          if !is_numeric {
            pad_start_order = false;
            break;
          }
        }
      } else {
        pad_start_order = false;
      }
    }
    let mut i = 0usize;
    while i < output_length {
      let max = (i + columns).min(output_length);
      let mut str_ = String::new();
      let mut j = i;
      while j < max - 1 {
        // Padding compensates for color escape codes (length vs width).
        let padding =
          max_line_length[j - i] + utf16_length(&output[j]) - data_len[j];
        let item = format!("{}, ", output[j]);
        str_.push_str(&pad_utf16(&item, padding, pad_start_order));
        j += 1;
      }
      if pad_start_order {
        let padding = max_line_length[j - i] + utf16_length(&output[j])
          - data_len[j]
          - separator_space;
        str_.push_str(&pad_utf16(&output[j], padding, true));
      } else {
        str_.push_str(&output[j]);
      }
      tmp.push(str_);
      i += columns;
    }
    if (ctx.max_array_length as usize) < output.len() {
      tmp.push(output[output_length].clone());
    }
    return tmp;
  }
  output
}
