Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 101 additions & 12 deletions core/engine/src/builtins/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Option<ShadowEntry>>,
pub(crate) location: IgnoreEq<ErrorLocation>,
}

impl Error {
Expand All @@ -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<ShadowEntry>) -> 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::<Self>(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);
Expand All @@ -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 =
Expand Down Expand Up @@ -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<JsValue> {
// 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::<Error>() 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::<String>();
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<JsValue> {
// 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.
Expand Down
78 changes: 14 additions & 64 deletions core/engine/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -209,7 +205,8 @@ macro_rules! js_error {
pub struct JsError {
inner: Repr,

pub(crate) backtrace: Option<Backtrace>,
#[unsafe_ignore_trace]
pub(crate) backtrace: Option<ShadowBacktrace>,
}

impl Eq for JsError {}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -558,7 +555,7 @@ impl JsError {
message,
cause: cause.map(|v| Box::new(Self::from_opaque(v))),
realm: Some(realm),
position,
location,
})
}
}
Expand Down Expand Up @@ -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("<anonymous>")?;
}

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("<anonymous>")?;
}

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(())
Expand Down Expand Up @@ -871,7 +821,7 @@ pub struct JsNativeError {
#[source]
cause: Option<Box<JsError>>,
realm: Option<Realm>,
position: IgnoreEq<Option<ShadowEntry>>,
location: IgnoreEq<ErrorLocation>,
}

impl fmt::Display for JsNativeError {
Expand All @@ -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(())
Expand Down Expand Up @@ -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(),
})),
}))),
}
}

Expand Down Expand Up @@ -1271,7 +1221,7 @@ impl JsNativeError {
message,
cause,
realm,
position,
location,
} = self;
let constructors = realm.as_ref().map_or_else(
|| context.intrinsics().constructors(),
Expand Down Expand Up @@ -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();

Expand Down
4 changes: 2 additions & 2 deletions core/engine/src/value/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ pub(crate) fn log_value_to(
.downcast_ref::<Error>()
.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::<Promise>() {
Expand Down
Loading
Loading