use crate::source::SourceSpan;
use std::error::Error as StdError;
use std::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
Syntax,
Type,
Reference,
Range,
JsInternal,
Internal,
Io,
Unsupported,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[repr(u8)]
pub enum NativeErrorKind {
Eval,
Range,
Reference,
Syntax,
Type,
Uri,
Internal,
Aggregate,
}
#[derive(Clone, Eq, PartialEq)]
pub struct NativeErrorMessage {
bytes: [u8; 256],
len: usize,
}
impl fmt::Debug for NativeErrorMessage {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("NativeErrorMessage")
.field(&&self.bytes[..self.len])
.finish()
}
}
impl Default for NativeErrorMessage {
fn default() -> Self {
Self::new()
}
}
impl NativeErrorMessage {
#[must_use]
pub fn new() -> Self {
Self {
bytes: [0; 256],
len: 0,
}
}
#[must_use]
pub fn from_utf8(value: &str) -> Self {
let mut message = Self::new();
message.push_bytes(value.bytes());
message
}
pub fn push_bytes(&mut self, bytes: impl IntoIterator<Item = u8>) {
for byte in bytes {
if self.len == self.bytes.len() - 1 {
break;
}
self.bytes[self.len] = byte;
self.len += 1;
}
}
pub fn push_c_string_bytes(&mut self, bytes: impl IntoIterator<Item = u8>) {
for byte in bytes {
if byte == 0 || self.len == self.bytes.len() - 1 {
break;
}
self.bytes[self.len] = byte;
self.len += 1;
}
}
pub fn push_utf8(&mut self, value: &str) {
self.push_bytes(value.bytes());
}
#[must_use]
pub fn visible_bytes(&self) -> &[u8] {
let visible_len = self.bytes[..self.len]
.iter()
.position(|byte| *byte == 0)
.unwrap_or(self.len);
&self.bytes[..visible_len]
}
}
impl NativeErrorKind {
pub const ALL: [Self; 8] = [
Self::Eval,
Self::Range,
Self::Reference,
Self::Syntax,
Self::Type,
Self::Uri,
Self::Internal,
Self::Aggregate,
];
pub const COUNT: usize = Self::ALL.len();
#[must_use]
pub const fn index(self) -> usize {
self as usize
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Eval => "EvalError",
Self::Range => "RangeError",
Self::Reference => "ReferenceError",
Self::Syntax => "SyntaxError",
Self::Type => "TypeError",
Self::Uri => "URIError",
Self::Internal => "InternalError",
Self::Aggregate => "AggregateError",
}
}
#[must_use]
pub const fn from_javascript_error(kind: ErrorKind) -> Option<Self> {
match kind {
ErrorKind::Syntax => Some(Self::Syntax),
ErrorKind::Type => Some(Self::Type),
ErrorKind::Reference => Some(Self::Reference),
ErrorKind::Range => Some(Self::Range),
ErrorKind::JsInternal => Some(Self::Internal),
ErrorKind::Unsupported | ErrorKind::Internal | ErrorKind::Io => None,
}
}
}
#[derive(Clone, Eq, PartialEq)]
struct ErrorData {
kind: ErrorKind,
message: String,
native_message: Option<Box<NativeErrorMessage>>,
span: Option<SourceSpan>,
}
#[derive(Clone, Eq, PartialEq)]
pub struct Error(Box<ErrorData>);
impl fmt::Debug for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Error")
.field("kind", &self.0.kind)
.field("message", &self.0.message)
.field("native_message", &self.0.native_message)
.field("span", &self.0.span)
.finish()
}
}
impl Error {
#[must_use]
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self(Box::new(ErrorData {
kind,
message: message.into(),
native_message: None,
span: None,
}))
}
#[must_use]
pub fn from_native_message(kind: ErrorKind, native_message: NativeErrorMessage) -> Self {
let message = native_message.to_utf8_lossy();
Self(Box::new(ErrorData {
kind,
message,
native_message: Some(Box::new(native_message)),
span: None,
}))
}
#[must_use]
pub fn syntax(message: impl Into<String>, span: SourceSpan) -> Self {
Self::new(ErrorKind::Syntax, message).with_span(span)
}
#[must_use]
pub fn unsupported(message: impl Into<String>, span: SourceSpan) -> Self {
Self::new(ErrorKind::Unsupported, message).with_span(span)
}
#[must_use]
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Internal, message)
}
#[must_use]
pub const fn kind(&self) -> ErrorKind {
self.0.kind
}
#[must_use]
pub fn message(&self) -> &str {
&self.0.message
}
#[must_use]
pub fn native_message(&self) -> Option<&NativeErrorMessage> {
self.0.native_message.as_deref()
}
#[must_use]
pub const fn span(&self) -> Option<SourceSpan> {
self.0.span
}
#[must_use]
pub const fn with_span(mut self, span: SourceSpan) -> Self {
self.0.span = Some(span);
self
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(span) = self.0.span {
if self.0.kind == ErrorKind::JsInternal {
write!(
formatter,
"InternalError at {}:{}: {}",
span.start.line, span.start.column, self.0.message
)
} else {
write!(
formatter,
"{:?}Error at {}:{}: {}",
self.0.kind, span.start.line, span.start.column, self.0.message
)
}
} else {
match self.0.kind {
ErrorKind::JsInternal => write!(formatter, "InternalError: {}", self.0.message),
_ => write!(formatter, "{:?}Error: {}", self.0.kind, self.0.message),
}
}
}
}
impl StdError for Error {}
#[cfg(test)]
mod tests {
use super::{Error, ErrorKind, NativeErrorMessage};
use crate::engine::api::core::RuntimeError;
use crate::source::{SourceLocation, SourceSpan};
#[test]
fn error_equality_and_debug_preserve_exact_native_payload() {
let mut raw = NativeErrorMessage::new();
raw.push_bytes([0x80, b'A']);
let exact = Error::from_native_message(ErrorKind::Type, raw);
let public = Error::new(ErrorKind::Type, "\u{fffd}");
assert_eq!(exact.message(), "\u{fffd}");
assert_ne!(exact, public);
assert_ne!(format!("{exact:?}"), format!("{public:?}"));
assert_eq!(
format!("{exact:?}"),
"Error { kind: Type, message: \"�\", native_message: Some(NativeErrorMessage([128, 65])), span: None }"
);
assert_eq!(format!("{exact}"), "TypeError: �");
assert_eq!(exact.clone(), exact);
assert_eq!(
exact.native_message().unwrap().visible_bytes(),
[0x80, b'A']
);
assert!(public.native_message().is_none());
}
#[test]
fn error_channel_stays_one_register() {
use std::mem::size_of;
let word = size_of::<usize>();
assert_eq!(size_of::<Error>(), word);
assert_eq!(size_of::<Result<(), Error>>(), word);
assert!(size_of::<Result<bool, Error>>() <= 2 * word);
assert!(size_of::<Result<&u8, Error>>() <= 2 * word);
}
#[test]
fn error_span_and_debug_survive_runtime_wrapping_and_clone() {
let span = SourceSpan::new(
SourceLocation::new(12, 3, 5),
SourceLocation::new(18, 3, 11),
);
let original = Error::syntax("unexpected token", span);
assert_eq!(original.kind(), ErrorKind::Syntax);
assert_eq!(original.message(), "unexpected token");
assert_eq!(original.span(), Some(span));
assert_eq!(
format!("{original}"),
"SyntaxError at 3:5: unexpected token"
);
assert_eq!(
format!("{original:?}"),
"Error { kind: Syntax, message: \"unexpected token\", native_message: None, span: Some(SourceSpan { start: SourceLocation { byte_offset: 12, line: 3, column: 5 }, end: SourceLocation { byte_offset: 18, line: 3, column: 11 } }) }"
);
let wrapped = RuntimeError::from(original.clone());
assert_eq!(wrapped, RuntimeError::Compile(original.clone()));
assert_eq!(format!("{wrapped}"), format!("{original}"));
assert_eq!(format!("{wrapped:?}"), format!("Compile({original:?})"));
assert_eq!(original.clone(), original);
let relocated = original.clone().with_span(SourceSpan::default());
assert_eq!(relocated.span(), Some(SourceSpan::default()));
assert_ne!(relocated, original);
assert_eq!(original.span(), Some(span));
}
#[test]
fn with_span_remains_usable_from_a_const_function() {
const fn attach_span(error: Error, span: SourceSpan) -> Error {
error.with_span(span)
}
let span = SourceSpan::new(SourceLocation::new(2, 1, 3), SourceLocation::new(5, 1, 6));
let error = attach_span(Error::new(ErrorKind::Unsupported, "frontier"), span);
assert_eq!(error.kind(), ErrorKind::Unsupported);
assert_eq!(error.message(), "frontier");
assert_eq!(error.span(), Some(span));
}
#[test]
fn native_error_payload_remains_exact_after_clone_and_span_change() {
let mut raw = NativeErrorMessage::new();
raw.push_bytes([b'A', 0, 0x80]);
let error = Error::from_native_message(ErrorKind::JsInternal, raw);
let span = SourceSpan::new(SourceLocation::new(4, 2, 1), SourceLocation::new(7, 2, 4));
let error = error.with_span(span);
let cloned = error.clone();
assert_eq!(cloned, error);
assert_eq!(cloned.kind(), ErrorKind::JsInternal);
assert_eq!(cloned.span(), Some(span));
assert_eq!(cloned.message(), "A");
assert_eq!(cloned.native_message().unwrap().visible_bytes(), b"A");
assert_eq!(format!("{cloned}"), "InternalError at 2:1: A");
assert!(format!("{cloned:?}").contains("NativeErrorMessage([65, 0, 128])"));
}
}