boa_engine 0.20.0

Boa is a Javascript lexer, parser and compiler written in Rust. Currently, it has support for some of the language.
Documentation
use super::{fmt, Display, HashSet, JsValue};
use crate::{
    builtins::{
        error::ErrorObject, map::ordered_map::OrderedMap, promise::PromiseState,
        set::ordered_set::OrderedSet, Array, Promise,
    },
    js_string,
    property::PropertyDescriptor,
    JsError, JsString,
};
use std::borrow::Cow;

/// This object is used for displaying a `Value`.
#[derive(Debug, Clone, Copy)]
pub struct ValueDisplay<'value> {
    pub(super) value: &'value JsValue,
    pub(super) internals: bool,
}

impl ValueDisplay<'_> {
    /// Display internal information about value.
    ///
    /// By default this is `false`.
    #[inline]
    #[must_use]
    pub const fn internals(mut self, yes: bool) -> Self {
        self.internals = yes;
        self
    }
}

/// A helper macro for printing objects
/// Can be used to print both properties and internal slots
/// All of the overloads take:
/// - The object to be printed
/// - The function with which to print
/// - The indentation for the current level (for nested objects)
/// - A `HashSet` with the addresses of the already printed objects for the current branch
///      (used to avoid infinite loops when there are cyclic deps)
macro_rules! print_obj_value {
    (all of $obj:expr, $display_fn:ident, $indent:expr, $encounters:expr) => {
        {
            let mut internals = print_obj_value!(internals of $obj, $display_fn, $indent, $encounters);
            let mut props = print_obj_value!(props of $obj, $display_fn, $indent, $encounters, true);

            props.reserve(internals.len());
            props.append(&mut internals);

            props
        }
    };
    (internals of $obj:expr, $display_fn:ident, $indent:expr, $encounters:expr) => {
        {
            let object = $obj.borrow();
            if let Some(object) = object.prototype() {
                vec![format!(
                    "{:>width$}: {}",
                    "__proto__",
                    $display_fn(&object.clone().into(), $encounters, $indent.wrapping_add(4), true),
                    width = $indent,
                )]
            } else {
                vec![format!(
                    "{:>width$}: {}",
                    "__proto__",
                    JsValue::Null.display(),
                    width = $indent,
                )]
            }
        }
    };
    (props of $obj:expr, $display_fn:ident, $indent:expr, $encounters:expr, $print_internals:expr) => {
        {let mut keys: Vec<_> = $obj.borrow().properties().index_property_keys().map(crate::property::PropertyKey::from).collect();
        keys.extend($obj.borrow().properties().shape.keys());
        let mut result = Vec::default();
        for key in keys {
            let val = $obj.borrow().properties().get(&key).expect("There should be a value");
            if val.is_data_descriptor() {
                let v = &val.expect_value();
                result.push(format!(
                    "{:>width$}: {}",
                    key,
                    $display_fn(v, $encounters, $indent.wrapping_add(4), $print_internals),
                    width = $indent,
                ));
            } else {
               let display = match (val.set().is_some(), val.get().is_some()) {
                    (true, true) => "Getter & Setter",
                    (true, false) => "Setter",
                    (false, true) => "Getter",
                    _ => "No Getter/Setter"
                };
               result.push(format!("{:>width$}: {}", key, display, width = $indent));
            }
        }
        result}
    };
}

pub(crate) fn log_string_from(x: &JsValue, print_internals: bool, print_children: bool) -> String {
    match x {
        // We don't want to print private (compiler) or prototype properties
        JsValue::Object(ref v) => {
            // Can use the private "type" field of an Object to match on
            // which type of Object it represents for special printing
            let v_bor = v.borrow();
            if let Some(s) = v_bor.downcast_ref::<JsString>() {
                format!("String {{ \"{}\" }}", s.to_std_string_escaped())
            } else if let Some(b) = v_bor.downcast_ref::<bool>() {
                format!("Boolean {{ {b} }}")
            } else if let Some(r) = v_bor.downcast_ref::<f64>() {
                if r.is_sign_negative() && *r == 0.0 {
                    "Number { -0 }".to_string()
                } else {
                    let mut buffer = ryu_js::Buffer::new();
                    format!("Number {{ {} }}", buffer.format(*r))
                }
            } else if v_bor.is::<Array>() {
                let len = v_bor
                    .properties()
                    .get(&js_string!("length").into())
                    .expect("array object must have 'length' property")
                    // FIXME: handle accessor descriptors
                    .expect_value()
                    .as_number()
                    .map(|n| n as i32)
                    .unwrap_or_default();

                if print_children {
                    if len == 0 {
                        return String::from("[]");
                    }

                    let arr = (0..len)
                        .map(|i| {
                            // Introduce recursive call to stringify any objects
                            // which are part of the Array

                            // FIXME: handle accessor descriptors
                            if let Some(value) = v_bor
                                .properties()
                                .get(&i.into())
                                .and_then(|x| x.value().cloned())
                            {
                                log_string_from(&value, print_internals, false)
                            } else {
                                String::from("<empty>")
                            }
                        })
                        .collect::<Vec<String>>()
                        .join(", ");

                    format!("[ {arr} ]")
                } else {
                    format!("Array({len})")
                }
            } else if let Some(map) = v_bor.downcast_ref::<OrderedMap<JsValue>>() {
                let size = map.len();
                if size == 0 {
                    return String::from("Map(0)");
                }

                if print_children {
                    let mappings = map
                        .iter()
                        .map(|(key, value)| {
                            let key = log_string_from(key, print_internals, false);
                            let value = log_string_from(value, print_internals, false);
                            format!("{key} → {value}")
                        })
                        .collect::<Vec<String>>()
                        .join(", ");
                    format!("Map {{ {mappings} }}")
                } else {
                    format!("Map({size})")
                }
            } else if let Some(set) = v_bor.downcast_ref::<OrderedSet>() {
                let size = set.len();

                if size == 0 {
                    return String::from("Set(0)");
                }

                if print_children {
                    let entries = set
                        .iter()
                        .map(|value| log_string_from(value, print_internals, false))
                        .collect::<Vec<String>>()
                        .join(", ");
                    format!("Set {{ {entries} }}")
                } else {
                    format!("Set({size})")
                }
            } else if v_bor.is::<ErrorObject>() {
                drop(v_bor);
                let name: Cow<'static, str> = v
                    .get_property(&js_string!("name").into())
                    .as_ref()
                    .and_then(PropertyDescriptor::value)
                    .map_or_else(
                        || "<error>".into(),
                        |v| {
                            v.as_string()
                                .map_or_else(
                                    || v.display().to_string(),
                                    JsString::to_std_string_escaped,
                                )
                                .into()
                        },
                    );
                let message = v
                    .get_property(&js_string!("message").into())
                    .as_ref()
                    .and_then(PropertyDescriptor::value)
                    .map(|v| {
                        v.as_string().map_or_else(
                            || v.display().to_string(),
                            JsString::to_std_string_escaped,
                        )
                    })
                    .unwrap_or_default();
                if name.is_empty() {
                    message
                } else if message.is_empty() {
                    name.to_string()
                } else {
                    format!("{name}: {message}")
                }
            } else if let Some(promise) = v_bor.downcast_ref::<Promise>() {
                format!(
                    "Promise {{ {} }}",
                    match promise.state() {
                        PromiseState::Pending => Cow::Borrowed("<pending>"),
                        PromiseState::Fulfilled(val) => Cow::Owned(val.display().to_string()),
                        PromiseState::Rejected(reason) => Cow::Owned(format!(
                            "<rejected> {}",
                            JsError::from_opaque(reason.clone())
                        )),
                    }
                )
            } else {
                x.display_obj(print_internals)
            }
        }
        _ => x.display().to_string(),
    }
}

impl JsValue {
    /// A helper function for specifically printing object values
    #[must_use]
    pub fn display_obj(&self, print_internals: bool) -> String {
        // A simple helper for getting the address of a value
        // TODO: Find a more general place for this, as it can be used in other situations as well
        fn address_of<T: ?Sized>(t: &T) -> usize {
            let my_ptr: *const T = t;
            my_ptr.cast::<()>() as usize
        }

        fn display_obj_internal(
            data: &JsValue,
            encounters: &mut HashSet<usize>,
            indent: usize,
            print_internals: bool,
        ) -> String {
            if let JsValue::Object(ref v) = *data {
                // The in-memory address of the current object
                let addr = address_of(v.as_ref());

                // We need not continue if this object has already been
                // printed up the current chain
                if encounters.contains(&addr) {
                    return String::from("[Cycle]");
                }

                // Mark the current object as encountered
                encounters.insert(addr);

                let result = if print_internals {
                    print_obj_value!(all of v, display_obj_internal, indent, encounters).join(",\n")
                } else {
                    print_obj_value!(props of v, display_obj_internal, indent, encounters, print_internals)
                        .join(",\n")
                };

                // If the current object is referenced in a different branch,
                // it will not cause an infinite printing loop, so it is safe to be printed again
                encounters.remove(&addr);

                let closing_indent = String::from_utf8(vec![b' '; indent.wrapping_sub(4)])
                    .expect("Could not create the closing brace's indentation string");

                format!("{{\n{result}\n{closing_indent}}}")
            } else {
                // Every other type of data is printed with the display method
                data.display().to_string()
            }
        }

        // We keep track of which objects we have encountered by keeping their
        // in-memory address in this set
        let mut encounters = HashSet::new();

        display_obj_internal(self, &mut encounters, 4, print_internals)
    }
}

impl Display for ValueDisplay<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.value {
            JsValue::Null => write!(f, "null"),
            JsValue::Undefined => write!(f, "undefined"),
            JsValue::Boolean(v) => write!(f, "{v}"),
            JsValue::Symbol(ref symbol) => {
                write!(f, "{}", symbol.descriptive_string().to_std_string_escaped())
            }
            JsValue::String(ref v) => write!(f, "\"{}\"", v.to_std_string_escaped()),
            JsValue::Rational(v) => format_rational(*v, f),
            JsValue::Object(_) => {
                write!(f, "{}", log_string_from(self.value, self.internals, true))
            }
            JsValue::Integer(v) => write!(f, "{v}"),
            JsValue::BigInt(ref num) => write!(f, "{num}n"),
        }
    }
}

/// This is different from the ECMAScript compliant number to string, in the printing of `-0`.
///
/// This function prints `-0` as `-0` instead of positive `0` as the specification says.
/// This is done to make it easer for the user of the REPL to identify what is a `-0` vs `0`,
/// since the REPL is not bound to the ECMAScript specification we can do this.
fn format_rational(v: f64, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    if v.is_sign_negative() && v == 0.0 {
        f.write_str("-0")
    } else {
        let mut buffer = ryu_js::Buffer::new();
        write!(f, "{}", buffer.format(v))
    }
}