diff --git a/core/engine/src/builtins/error/mod.rs b/core/engine/src/builtins/error/mod.rs index 9f093fe67e0..e1bd9c9381d 100644 --- a/core/engine/src/builtins/error/mod.rs +++ b/core/engine/src/builtins/error/mod.rs @@ -20,7 +20,7 @@ use crate::{ property::Attribute, realm::Realm, string::StaticJsStrings, - vm::shadow_stack::ShadowEntry, + vm::shadow_stack::ErrorLocation, }; use boa_gc::{Finalize, Trace}; use boa_macros::js_str; @@ -136,7 +136,7 @@ pub struct Error { // The position of where the Error was created does not affect equality check. #[unsafe_ignore_trace] - pub(crate) position: IgnoreEq>, + pub(crate) location: IgnoreEq, } impl Error { @@ -146,34 +146,53 @@ impl Error { pub fn new(tag: ErrorKind) -> Self { Self { tag, - position: IgnoreEq(None), + location: IgnoreEq(ErrorLocation::Position(None)), } } - /// Create a new [`Error`] with the given optional [`ShadowEntry`]. - pub(crate) fn with_shadow_entry(tag: ErrorKind, entry: Option) -> Self { + /// Create a new [`Error`] with the given [`ErrorLocation`]. + pub(crate) fn with_location(tag: ErrorKind, location: ErrorLocation) -> Self { Self { tag, - position: IgnoreEq(entry), + location: IgnoreEq(location), } } /// Get the position from the last called function. pub(crate) fn with_caller_position(tag: ErrorKind, context: &Context) -> Self { + let limit = context.runtime_limits().backtrace_limit(); + let backtrace = context.vm.shadow_stack.caller_position(limit); Self { tag, - position: IgnoreEq(context.vm.shadow_stack.caller_position()), + location: IgnoreEq(ErrorLocation::Backtrace(backtrace)), } } } impl IntrinsicObject for Error { fn init(realm: &Realm) { - let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + let property_attribute = + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + let accessor_attribute = Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + + let get_stack = BuiltInBuilder::callable(realm, Self::get_stack) + .name(js_string!("get stack")) + .build(); + + let set_stack = BuiltInBuilder::callable(realm, Self::set_stack) + .name(js_string!("set stack")) + .build(); + let builder = BuiltInBuilder::from_standard_constructor::(realm) - .property(js_string!("name"), Self::NAME, attribute) - .property(js_string!("message"), js_string!(), attribute) - .method(Self::to_string, js_string!("toString"), 0); + .property(js_string!("name"), Self::NAME, property_attribute) + .property(js_string!("message"), js_string!(), property_attribute) + .method(Self::to_string, js_string!("toString"), 0) + .accessor( + js_string!("stack"), + Some(get_stack), + Some(set_stack), + accessor_attribute, + ); #[cfg(feature = "experimental")] let builder = builder.static_method(Error::is_error, js_string!("isError"), 1); @@ -192,7 +211,7 @@ impl BuiltInObject for Error { impl BuiltInConstructor for Error { const CONSTRUCTOR_ARGUMENTS: usize = 1; - const PROTOTYPE_STORAGE_SLOTS: usize = 3; + const PROTOTYPE_STORAGE_SLOTS: usize = 5; const CONSTRUCTOR_STORAGE_SLOTS: usize = 1; const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = @@ -263,6 +282,76 @@ impl Error { Ok(()) } + /// `get Error.prototype.stack` + /// + /// The accessor property of Error instances represents the stack trace + /// when the error was created. + /// + /// More information: + /// - [Proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-error-stacks/ + fn get_stack(this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult { + // 1. Let E be the this value. + // 2. If E is not an Object, return undefined. + let Some(e) = this.as_object() else { + return Ok(JsValue::undefined()); + }; + + // 3. Let errorData be the value of the [[ErrorData]] internal slot of E. + // 4. If errorData is undefined, return undefined. + let Some(error_data) = e.downcast_ref::() else { + return Ok(JsValue::undefined()); + }; + + // 5. Let stackString be an implementation-defined String value representing the call stack. + // 6. Return stackString. + if let Some(backtrace) = error_data.location.0.backtrace() { + let stack_string = backtrace + .iter() + .rev() + .map(|entry| format!(" at {}\n", entry.display(true))) + .collect::(); + return Ok(js_string!(stack_string).into()); + } + + // 7. If no stack trace is available, return undefined. + Ok(JsValue::undefined()) + } + + /// `set Error.prototype.stack` + /// + /// The setter for the stack property. + /// + /// More information: + /// - [Proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-error-stacks/ + fn set_stack(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let E be the this value. + // 2. If Type(E) is not Object, throw a TypeError exception. + let e = this.as_object().ok_or_else(|| { + JsNativeError::typ() + .with_message("Error.prototype.stack setter requires that 'this' be an Object") + })?; + + // 3. Let numberOfArgs be the number of arguments passed to this function call. + let number_of_args = args.len(); + + // 4. If numberOfArgs is 0, throw a TypeError exception. + if number_of_args == 0 { + return Err(JsNativeError::typ() + .with_message( + "Error.prototype.stack setter requires at least 1 argument, but only 0 were passed", + ) + .into()); + } + + // 5. Return ? CreateDataPropertyOrThrow(E, "stack", value). + e.create_data_property_or_throw(js_string!("stack"), args[0].clone(), context) + .map(Into::into) + } + /// `Error.prototype.toString()` /// /// The `toString()` method returns a string representing the specified Error object. diff --git a/core/engine/src/error.rs b/core/engine/src/error.rs index 4c24ad51107..3bc0b1b4fbf 100644 --- a/core/engine/src/error.rs +++ b/core/engine/src/error.rs @@ -12,15 +12,11 @@ use crate::{ realm::Realm, vm::{ NativeSourceInfo, - shadow_stack::{Backtrace, ShadowEntry}, + shadow_stack::{Backtrace as ShadowBacktrace, ErrorLocation, ShadowEntry}, }, }; use boa_gc::{Finalize, Trace, custom_trace}; -use std::{ - borrow::Cow, - error, - fmt::{self}, -}; +use std::{borrow::Cow, error, fmt}; use thiserror::Error; /// Create an error object from a value or string literal. Optionally the @@ -209,7 +205,8 @@ macro_rules! js_error { pub struct JsError { inner: Repr, - pub(crate) backtrace: Option, + #[unsafe_ignore_trace] + pub(crate) backtrace: Option, } impl Eq for JsError {} @@ -503,7 +500,7 @@ impl JsError { let cause = try_get_property(js_string!("cause"), "cause", context)?; - let position = error_data.position.clone(); + let location = error_data.location.clone(); let kind = match error_data.tag { ErrorKind::Error => JsNativeErrorKind::Error, ErrorKind::Eval => JsNativeErrorKind::Eval, @@ -558,7 +555,7 @@ impl JsError { message, cause: cause.map(|v| Box::new(Self::from_opaque(v))), realm: Some(realm), - position, + location, }) } } @@ -764,54 +761,7 @@ impl fmt::Display for JsError { if let Some(shadow_stack) = &self.backtrace { for entry in shadow_stack.iter().rev() { - write!(f, "\n at ")?; - match entry { - ShadowEntry::Native { - function_name, - source_info, - } => { - if let Some(function_name) = function_name { - write!(f, "{}", function_name.to_std_string_escaped())?; - } else { - f.write_str("")?; - } - - if let Some(loc) = source_info.as_location() { - write!( - f, - " (native at {}:{}:{})", - loc.file(), - loc.line(), - loc.column() - )?; - } else { - f.write_str(" (native)")?; - } - } - ShadowEntry::Bytecode { pc, source_info } => { - let has_function_name = !source_info.function_name().is_empty(); - if has_function_name { - write!(f, "{}", source_info.function_name().to_std_string_escaped(),)?; - } else { - f.write_str("")?; - } - - f.write_str(" (")?; - source_info.map().path().fmt(f)?; - - if let Some(position) = source_info.map().find(*pc) { - write!( - f, - ":{}:{}", - position.line_number(), - position.column_number() - )?; - } else { - f.write_str(":?:?")?; - } - f.write_str(")")?; - } - } + write!(f, "\n at {}", entry.display(true))?; } } Ok(()) @@ -871,7 +821,7 @@ pub struct JsNativeError { #[source] cause: Option>, realm: Option, - position: IgnoreEq>, + location: IgnoreEq, } impl fmt::Display for JsNativeError { @@ -883,8 +833,8 @@ impl fmt::Display for JsNativeError { write!(f, ": {message}")?; } - if let Some(position) = &self.position.0 { - position.fmt(f)?; + if let Some(entry) = self.location.0.position() { + write!(f, "{}", entry.display(false))?; } Ok(()) @@ -945,10 +895,10 @@ impl JsNativeError { message, cause, realm: None, - position: IgnoreEq(Some(ShadowEntry::Native { + location: IgnoreEq(ErrorLocation::Position(Some(ShadowEntry::Native { function_name: None, source_info: NativeSourceInfo::caller(), - })), + }))), } } @@ -1271,7 +1221,7 @@ impl JsNativeError { message, cause, realm, - position, + location, } = self; let constructors = realm.as_ref().map_or_else( || context.intrinsics().constructors(), @@ -1299,7 +1249,7 @@ impl JsNativeError { let o = JsObject::from_proto_and_data_with_shared_shape( context.root_shape(), prototype, - Error::with_shadow_entry(tag, position.0.clone()), + Error::with_location(tag, location.0.clone()), ) .upcast(); diff --git a/core/engine/src/value/display.rs b/core/engine/src/value/display.rs index 6a33df760e2..c9c6038c0ec 100644 --- a/core/engine/src/value/display.rs +++ b/core/engine/src/value/display.rs @@ -257,8 +257,8 @@ pub(crate) fn log_value_to( .downcast_ref::() .expect("already checked object type"); - if let Some(position) = &data.position.0 { - write!(f, "{position}")?; + if let Some(entry) = data.location.0.position() { + write!(f, "{}", entry.display(false))?; } Ok(()) } else if let Some(promise) = v.downcast_ref::() { diff --git a/core/engine/src/vm/shadow_stack.rs b/core/engine/src/vm/shadow_stack.rs index 6b27541e03a..34a96860104 100644 --- a/core/engine/src/vm/shadow_stack.rs +++ b/core/engine/src/vm/shadow_stack.rs @@ -1,4 +1,4 @@ -use std::fmt::{Display, Write}; +use std::fmt::{self, Display}; use boa_gc::{Finalize, Trace}; use boa_string::JsString; @@ -19,6 +19,28 @@ impl Backtrace { } } +#[derive(Debug, Clone, Trace, Finalize)] +pub(crate) enum ErrorLocation { + Position(#[unsafe_ignore_trace] Option), + Backtrace(#[unsafe_ignore_trace] Backtrace), +} + +impl ErrorLocation { + pub(crate) fn backtrace(&self) -> Option<&Backtrace> { + match self { + Self::Backtrace(bt) => Some(bt), + Self::Position(_) => None, + } + } + + pub(crate) fn position(&self) -> Option<&ShadowEntry> { + match self { + Self::Position(pos) => pos.as_ref(), + Self::Backtrace(bt) => bt.iter().next(), + } + } +} + #[derive(Debug, Clone)] pub(crate) enum ShadowEntry { Native { @@ -31,42 +53,77 @@ pub(crate) enum ShadowEntry { }, } -impl Display for ShadowEntry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { +impl ShadowEntry { + /// Create a display wrapper for this entry. + /// + /// # Arguments + /// + /// * `show_function_name` - Whether to include the function name in the output. + pub(crate) fn display(&self, show_function_name: bool) -> DisplayShadowEntry<'_> { + DisplayShadowEntry { + entry: self, + show_function_name, + } + } +} + +/// Helper struct to format a shadow entry for display. +pub(crate) struct DisplayShadowEntry<'a> { + entry: &'a ShadowEntry, + show_function_name: bool, +} + +impl Display for DisplayShadowEntry<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.entry { ShadowEntry::Native { function_name, source_info, } => { - if function_name.is_some() || source_info.as_location().is_some() { - f.write_str(" (native")?; + if self.show_function_name { if let Some(function_name) = function_name { - write!(f, " {}", function_name.to_std_string_escaped())?; + write!(f, "{}", function_name.to_std_string_escaped())?; + } else { + f.write_str("")?; } - if let Some(location) = source_info.as_location() { - write!(f, " at {location}")?; - } - f.write_char(')')?; + } + + if let Some(loc) = source_info.as_location() { + write!( + f, + " (native at {}:{}:{})", + loc.file(), + loc.line(), + loc.column() + )?; + } else { + f.write_str(" (native)")?; } } ShadowEntry::Bytecode { pc, source_info } => { - let path = source_info.map().path(); - let position = source_info.map().find(*pc); - - if path.is_some() || position.is_some() { - write!(f, " ({}", source_info.map().path())?; - - if let Some(position) = position { - write!( - f, - ":{}:{}", - position.line_number(), - position.column_number() - )?; + if self.show_function_name { + let has_function_name = !source_info.function_name().is_empty(); + if has_function_name { + write!(f, "{}", source_info.function_name().to_std_string_escaped())?; + } else { + f.write_str("
")?; } - - f.write_char(')')?; } + f.write_str(" (")?; + + source_info.map().path().fmt(f)?; + + if let Some(position) = source_info.map().find(*pc) { + write!( + f, + ":{}:{}", + position.line_number(), + position.column_number() + )?; + } else { + f.write_str(":?:?")?; + } + f.write_str(")")?; } } Ok(()) @@ -131,10 +188,19 @@ impl ShadowStack { Backtrace { stack } } - pub(crate) fn caller_position(&self) -> Option { - // NOTE: We push the function that is currently execution, so the second last is the caller. - let index = self.stack.len().checked_sub(2)?; - self.stack.get(index).cloned() + pub(crate) fn caller_position(&self, n: usize) -> Backtrace { + // NOTE: We push the function that is currently executing, so skip the last one. + let stack = self + .stack + .iter() + .rev() + .skip(1) + .take(n) + .rev() + .cloned() + .collect::>(); + + Backtrace { stack } } #[cfg(feature = "native-backtrace")]