use std::{
collections::hash_map::Entry,
error::Error as StdError,
fmt::{Debug, Display, Formatter},
ops::Range,
result::Result as StdResult,
str::Utf8Error,
sync::Arc,
};
use ahash::{AHashMap, AHashSet};
use lady_deirdre::{arena::Identifiable, format::AnnotationPriority, lexis::ToSpan};
use crate::{
analysis::ModuleTextResolver,
format::{format_script_path, ScriptSnippet},
runtime::{ops::OperatorKind, Origin, TypeMeta},
};
pub type RuntimeResult<T> = StdResult<T, RuntimeError>;
pub trait RuntimeResultExt {
type OkType;
fn expect_blame(self, message: &str) -> Self::OkType;
}
impl<T> RuntimeResultExt for RuntimeResult<T> {
type OkType = T;
#[inline(always)]
fn expect_blame(self, message: &str) -> Self::OkType {
match self {
Ok(ok) => ok,
Err(error) => {
let origin = *error.primary_origin();
match origin {
Origin::Rust(origin) => origin.blame(&format!("{message}\n{error}")),
Origin::Script(origin) => {
panic!(
"{}: {message}\n{error}",
format_script_path(origin.id(), None),
);
}
}
}
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum RuntimeError {
Nil {
access_origin: Origin,
},
NonSingleton {
access_origin: Origin,
actual: usize,
},
ShortSlice {
access_origin: Origin,
minimum: usize,
actual: usize,
},
OutOfBounds {
access_origin: Origin,
index: usize,
length: usize,
},
ReadOnly {
access_origin: Origin,
data_origin: Origin,
},
WriteOnly {
access_origin: Origin,
data_origin: Origin,
},
ReadToWrite {
access_origin: Origin,
borrow_origin: Origin,
},
WriteToRead {
access_origin: Origin,
borrow_origin: Origin,
},
WriteToWrite {
access_origin: Origin,
borrow_origin: Origin,
},
Utf8Decoding {
access_origin: Origin,
cause: Box<Utf8Error>,
},
BorrowLimit {
access_origin: Origin,
limit: usize,
},
TypeMismatch {
access_origin: Origin,
data_type: &'static TypeMeta,
expected_types: Vec<&'static TypeMeta>,
},
DowncastStatic {
access_origin: Origin,
},
UpcastResult {
access_origin: Origin,
cause: Arc<dyn StdError + Send + Sync + 'static>,
},
NumberCast {
access_origin: Origin,
from: &'static TypeMeta,
to: &'static TypeMeta,
cause: NumberCastCause,
value: Arc<dyn NumValue>,
},
NumericOperation {
invoke_origin: Origin,
kind: NumericOperationKind,
lhs: (&'static TypeMeta, Arc<dyn NumValue>),
rhs: Option<(&'static TypeMeta, Arc<dyn NumValue>)>,
target: &'static TypeMeta,
},
RangeCast {
access_origin: Origin,
from: Range<usize>,
to: &'static str,
},
MalformedRange {
access_origin: Origin,
start_bound: usize,
end_bound: usize,
},
PrimitiveParse {
access_origin: Origin,
from: String,
to: &'static TypeMeta,
cause: Arc<dyn StdError + Send + Sync + 'static>,
},
ArityMismatch {
invocation_origin: Origin,
function_origin: Origin,
parameters: usize,
arguments: usize,
},
UndefinedOperator {
access_origin: Origin,
receiver_origin: Option<Origin>,
receiver_type: &'static TypeMeta,
operator: OperatorKind,
},
UnknownField {
access_origin: Origin,
receiver_origin: Origin,
receiver_type: &'static TypeMeta,
field: String,
},
FormatError {
access_origin: Origin,
receiver_origin: Origin,
},
UnknownPackage {
access_origin: Origin,
name: &'static str,
version: &'static str,
},
Interrupted {
origin: Origin,
},
StackOverflow {
origin: Origin,
},
}
impl Display for RuntimeError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Nil { .. } => formatter.write_str("inaccessible data"),
Self::NonSingleton { actual, .. } => formatter.write_fmt(format_args!(
"expected a single data instance, but the array with {actual} \
elements provided",
)),
Self::ShortSlice {
minimum, actual, ..
} => formatter.write_fmt(format_args!(
"expected an array with at least {minimum} elements, but the \
array with {actual} elements provided",
)),
Self::OutOfBounds { index, length, .. } => {
formatter.write_fmt(format_args!("index {index} out of 0..{length} bounds",))
}
Self::ReadOnly { .. } => formatter.write_str("read-only data"),
Self::WriteOnly { .. } => formatter.write_str("write-only data"),
Self::ReadToWrite { .. } => {
formatter.write_str("cannot access data for write while it is being read")
}
Self::WriteToRead { .. } => {
formatter.write_str("cannot access data for read while it is being written")
}
Self::WriteToWrite { .. } => {
formatter.write_str("cannot access data for write more than once")
}
Self::Utf8Decoding { .. } => formatter.write_str("invalid utf-8 encoding"),
Self::BorrowLimit { .. } => formatter.write_str("too many simultaneous data accesses"),
Self::TypeMismatch {
data_type,
expected_types,
..
} => {
let partition = Self::partition_types(expected_types);
match partition.is_empty() {
true => formatter.write_fmt(format_args!("unexpected '{data_type}' data type")),
false => {
let partition = partition.join(", or ");
formatter.write_fmt(format_args!(
"expected {partition}, but '{data_type}' data type provided"
))
}
}
}
Self::DowncastStatic { .. } => formatter
.write_str("cannot get static reference to the data owned by the script engine"),
Self::UpcastResult { .. } => {
formatter.write_str("the function returned explicit error")
}
Self::NumberCast {
from,
to,
cause,
value,
..
} => {
use NumberCastCause::*;
match cause {
Infinite => formatter.write_fmt(format_args!(
"cannot cast infinity value of {from} type to {to}"
)),
NAN => formatter
.write_fmt(format_args!("cannot cast NAN value of {from} type to {to}")),
Overflow => {
formatter.write_fmt(format_args!("cannot cast {value}{from} to {to} type"))
}
Underflow => {
formatter.write_fmt(format_args!("cannot cast {value}{from} to {to} type"))
}
}
}
Self::NumericOperation { kind, lhs, rhs, .. } => {
use NumericOperationKind::*;
match (lhs, kind, rhs) {
((lhs_ty, lhs_value), Add, Some((rhs_ty, rhs_value))) => formatter.write_fmt(
format_args!("cannot add {rhs_value}{rhs_ty} to {lhs_value}{lhs_ty}"),
),
((lhs_ty, lhs_value), Sub, Some((rhs_ty, rhs_value))) => {
formatter.write_fmt(format_args!(
"cannot subtract {rhs_value}{rhs_ty} from {lhs_value}{lhs_ty}"
))
}
((lhs_ty, lhs_value), Mul, Some((rhs_ty, rhs_value))) => formatter.write_fmt(
format_args!("cannot multiply {rhs_value}{rhs_ty} by {lhs_value}{lhs_ty}"),
),
((lhs_ty, lhs_value), Div, Some((rhs_ty, rhs_value))) => formatter.write_fmt(
format_args!("cannot divide {rhs_value}{rhs_ty} by {lhs_value}{lhs_ty}"),
),
((lhs_ty, lhs_value), Neg, None) => formatter.write_fmt(format_args!(
"cannot get negative number of {lhs_value}{lhs_ty}"
)),
((lhs_ty, lhs_value), Shl, Some((rhs_ty, rhs_value))) => {
formatter.write_fmt(format_args!(
"cannot shift {lhs_value}{lhs_ty} left by {rhs_value}{rhs_ty} bits"
))
}
((lhs_ty, lhs_value), Shr, Some((rhs_ty, rhs_value))) => {
formatter.write_fmt(format_args!(
"cannot shift {lhs_value}{lhs_ty} right by {rhs_value}{rhs_ty} bits"
))
}
((lhs_ty, lhs_value), Rem, Some((_rhs_ty, _rhs_value))) => {
formatter.write_fmt(format_args!(
"cannot get {lhs_value}{lhs_ty} reminder of \
division by {lhs_value}{lhs_ty}"
))
}
_ => formatter.write_str("invalid numeric operation"),
}
}
Self::RangeCast { from, to, .. } => formatter.write_fmt(format_args!(
"cannot cast {from:?} range to {to} type. target type bounds mismatch"
)),
Self::MalformedRange {
start_bound,
end_bound,
..
} => formatter.write_fmt(format_args!(
"range {start_bound} start bound is greater than the range end bound {end_bound}"
)),
Self::PrimitiveParse { from, to, .. } => {
formatter.write_fmt(format_args!("failed to parse {from:?} as {to}"))
}
Self::ArityMismatch {
parameters,
arguments,
..
} => match *parameters == 1 {
true => formatter.write_fmt(format_args!(
"the function requires 1 argument, but {arguments} provided"
)),
false => formatter.write_fmt(format_args!(
"the function requires {parameters} arguments, but {arguments} provided"
)),
},
Self::UndefinedOperator {
receiver_type,
operator,
..
} => formatter.write_fmt(format_args!(
"type '{receiver_type}' does not implement {operator}"
)),
Self::UnknownField {
receiver_type,
field,
..
} => formatter.write_fmt(format_args!(
"type '{receiver_type}' does not have field '{field}'"
)),
Self::FormatError { .. } => formatter
.write_str("an error occurred during Debug or Display format function call"),
Self::UnknownPackage { name, version, .. } => {
formatter.write_fmt(format_args!("unknown {name}@{version} script package"))
}
Self::Interrupted { .. } => formatter.write_str("script evaluation interrupted"),
Self::StackOverflow { .. } => formatter.write_str("script engine stack overflow"),
}
}
}
impl StdError for RuntimeError {
#[inline]
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Utf8Decoding { cause, .. } => Some(cause),
Self::UpcastResult { cause, .. } => Some(cause),
Self::PrimitiveParse { cause, .. } => Some(cause),
_ => None,
}
}
}
impl RuntimeError {
pub fn display<'a>(&self, resolver: &'a impl ModuleTextResolver) -> impl Display + 'a {
enum DisplayError<'a> {
Snippet(ScriptSnippet<'a>),
String(String),
}
impl<'a> Display for DisplayError<'a> {
#[inline(always)]
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Snippet(display) => Display::fmt(display, formatter),
Self::String(display) => Display::fmt(display, formatter),
}
}
}
let primary_description = self.primary_description();
let Origin::Script(primary_origin) = self.primary_origin() else {
return DisplayError::String(primary_description);
};
let Some(primary_text) = resolver.resolve(primary_origin.id()) else {
return DisplayError::String(primary_description);
};
if !primary_origin.is_valid_span(primary_text) {
return DisplayError::String(primary_description);
}
let secondary_description = self.secondary_description();
let mut snippet = primary_text.snippet();
snippet.set_caption("runtime error");
snippet.annotate(
primary_origin,
AnnotationPriority::Primary,
primary_description,
);
let mut summary = self.summary();
match self.secondary_origin() {
Some(Origin::Script(secondary_origin))
if secondary_origin.id() == primary_origin.id() =>
{
snippet.annotate(
secondary_origin,
AnnotationPriority::Secondary,
secondary_description,
);
}
Some(Origin::Script(secondary_origin)) => {
if let Some(secondary_text) = resolver.resolve(primary_origin.id()) {
if secondary_origin.is_valid_span(secondary_text) {
summary.push_str("\n\n");
let mut inner_snippet = secondary_text.snippet();
inner_snippet.annotate(
secondary_origin,
AnnotationPriority::Secondary,
secondary_description,
);
summary.push_str(&inner_snippet.to_string());
}
}
}
Some(Origin::Rust(secondary_origin)) => {
if let Some(code) = secondary_origin.code {
summary.push_str(&format!("\n\n{code}: {secondary_description}"));
}
}
None => (),
}
snippet.set_summary(summary);
DisplayError::Snippet(snippet)
}
pub fn primary_origin(&self) -> &Origin {
match self {
Self::Nil { access_origin, .. } => access_origin,
Self::NonSingleton { access_origin, .. } => access_origin,
Self::ShortSlice { access_origin, .. } => access_origin,
Self::OutOfBounds { access_origin, .. } => access_origin,
Self::ReadOnly { access_origin, .. } => access_origin,
Self::WriteOnly { access_origin, .. } => access_origin,
Self::ReadToWrite { access_origin, .. } => access_origin,
Self::WriteToRead { access_origin, .. } => access_origin,
Self::WriteToWrite { access_origin, .. } => access_origin,
Self::Utf8Decoding { access_origin, .. } => access_origin,
Self::BorrowLimit { access_origin, .. } => access_origin,
Self::TypeMismatch { access_origin, .. } => access_origin,
Self::DowncastStatic { access_origin, .. } => access_origin,
Self::UpcastResult { access_origin, .. } => access_origin,
Self::NumberCast { access_origin, .. } => access_origin,
Self::RangeCast { access_origin, .. } => access_origin,
Self::MalformedRange { access_origin, .. } => access_origin,
Self::NumericOperation { invoke_origin, .. } => invoke_origin,
Self::PrimitiveParse { access_origin, .. } => access_origin,
Self::ArityMismatch {
invocation_origin, ..
} => invocation_origin,
Self::UndefinedOperator { access_origin, .. } => access_origin,
Self::UnknownField { access_origin, .. } => access_origin,
Self::FormatError { access_origin, .. } => access_origin,
Self::UnknownPackage { access_origin, .. } => access_origin,
Self::Interrupted { origin } => origin,
Self::StackOverflow { origin, .. } => origin,
}
}
pub fn secondary_origin(&self) -> Option<&Origin> {
match self {
Self::Nil { .. } => None,
Self::NonSingleton { .. } => None,
Self::ShortSlice { .. } => None,
Self::OutOfBounds { .. } => None,
Self::ReadOnly { data_origin, .. } => Some(data_origin),
Self::WriteOnly { data_origin, .. } => Some(data_origin),
Self::ReadToWrite { borrow_origin, .. } => Some(borrow_origin),
Self::WriteToRead { borrow_origin, .. } => Some(borrow_origin),
Self::WriteToWrite { borrow_origin, .. } => Some(borrow_origin),
Self::Utf8Decoding { .. } => None,
Self::BorrowLimit { .. } => None,
Self::TypeMismatch { .. } => None,
Self::DowncastStatic { .. } => None,
Self::UpcastResult { .. } => None,
Self::NumberCast { .. } => None,
Self::NumericOperation { .. } => None,
Self::RangeCast { .. } => None,
Self::MalformedRange { .. } => None,
Self::PrimitiveParse { .. } => None,
Self::ArityMismatch {
function_origin, ..
} => Some(function_origin),
Self::UndefinedOperator {
receiver_origin, ..
} => receiver_origin.as_ref(),
Self::UnknownField {
receiver_origin, ..
} => Some(receiver_origin),
Self::FormatError {
receiver_origin, ..
} => Some(receiver_origin),
Self::UnknownPackage { .. } => None,
Self::Interrupted { .. } => None,
Self::StackOverflow { .. } => None,
}
}
#[inline(always)]
pub fn primary_description(&self) -> String {
self.to_string()
}
pub fn secondary_description(&self) -> String {
match self {
Self::Nil { .. } => String::new(),
Self::NonSingleton { .. } => String::new(),
Self::ShortSlice { .. } => String::new(),
Self::OutOfBounds { .. } => String::new(),
Self::ReadOnly { .. } => String::from("data object origin"),
Self::WriteOnly { .. } => String::from("data object origin"),
Self::ReadToWrite { .. } => String::from("active read access"),
Self::WriteToRead { .. } => String::from("active write access"),
Self::WriteToWrite { .. } => String::from("active write access"),
Self::Utf8Decoding { .. } => String::new(),
Self::BorrowLimit { .. } => String::new(),
Self::TypeMismatch { .. } => String::new(),
Self::DowncastStatic { .. } => String::new(),
Self::UpcastResult { .. } => String::new(),
Self::NumberCast { .. } => String::new(),
Self::NumericOperation { .. } => String::new(),
Self::RangeCast { .. } => String::new(),
Self::MalformedRange { .. } => String::new(),
Self::PrimitiveParse { .. } => String::new(),
Self::ArityMismatch { .. } => String::from("function origin"),
Self::UndefinedOperator {
receiver_origin, ..
} if receiver_origin.is_some() => String::from("receiver origin"),
Self::UndefinedOperator { .. } => String::new(),
Self::UnknownField { .. } => String::from("receiver origin"),
Self::FormatError { .. } => String::from("receiver object"),
Self::UnknownPackage { .. } => String::new(),
Self::Interrupted { .. } => String::new(),
Self::StackOverflow { .. } => String::new(),
}
}
pub fn summary(&self) -> String {
let result = match self {
Self::Nil { .. } => {
r#"The requested operation has been applied on the void data.
The source of the void data could be:
- an empty array "[]",
- a struct field that does not exists in the struct,
- a function or operator that does not return any value,
- a function that returns "Option::None",
- or any other source.
Use the ? operator to check if the value is void: "if a? {...}"."#
}
Self::NonSingleton { .. } => {
r#"Most script operations require singleton objects (just normal objects)
and cannot be applied to arrays with zero or more than one element.
Consider using the index operator to retrieve a single element from the array:
"my_array[3] + 10" instead of "my_array + 10"."#
}
Self::ShortSlice { .. } => {
r#"The underlying operation requires an array of longer length."#
}
Self::OutOfBounds { .. } => {
r#"The specified range or an index is out of the array bounds."#
}
Self::ReadOnly { .. } => {
r#"The underlying operation requires write access to one of its arguments,
but the argument reference provides read-only access."#
}
Self::WriteOnly { .. } => {
r#"The underlying operation requires read access to one of its arguments,
but the argument reference provides write-only access."#
}
Self::ReadToWrite { .. } => {
r#"The underlying operation requires write access to one of its arguments,
but the argument object is currently being read.
The script engine does not allow simultaneous read and write access
to the same data.
For instance, if the script calls a function (or an operator) with this object
as an argument and the function returns a reference that indirectly points
to the argument's data, the object is blocked for writing until the reference's
lifetime ends."#
}
Self::WriteToRead { .. } => {
r#"The underlying operation requires read access to one of its arguments,
but the argument object is currently being written.
The script engine does not allow simultaneous read and write access
to the same data.
For instance, if the script calls a function (or an operator) with this object
as an argument and the function returns a reference that indirectly modifies
argument's data, the object is blocked for reading until the reference's
lifetime ends."#
}
Self::WriteToWrite { .. } => {
r#"The underlying operation requires write access to one of its arguments,
but the argument object is currently being written.
The script engine mandates that the ongoing write access to the data object
is exclusive.
For instance, if the script calls a function (or an operator) with this object
as an argument and the function returns a reference that indirectly modifies
argument's data, the object is blocked from another write access until
the reference's lifetime ends."#
}
Self::Utf8Decoding { cause, .. } => {
let mut result = String::from(
r#"The underlying operation is attempting to reinterpret an array of bytes
as a UTF-8 encoding of the Unicode text, but the script engine has detected
that this encoding is invalid.
Error description:"#,
);
for line in cause.to_string().split("\n") {
result.push_str("\n ");
result.push_str(line);
}
return result;
}
Self::BorrowLimit { .. } => {
r#"The script engine has a predefined limit for active references
to the same data object.
This limit may be exceeded, for example, if a recursive function attempts
to access the same object too many times."#
}
Self::TypeMismatch { .. } => {
r#"The underlying function (or operator) requires an argument of a different type
than the one being provided."#
}
Self::DowncastStatic { .. } => {
r#"The underlying function (or operator) requested a data object
with a lifetime that may be different from the actual object's lifetime."#
}
Self::UpcastResult { cause, .. } => {
let mut result = String::from(
r#"The invoked function (or operator) returned explicit error.
Error description:"#,
);
for line in cause.to_string().split("\n") {
result.push_str("\n ");
result.push_str(line);
}
return result;
}
Self::NumberCast { cause, .. } => match cause {
NumberCastCause::Infinite | NumberCastCause::NAN => {
r#"Failed to cast primitive numeric type to another numeric type."#
}
NumberCastCause::Overflow => {
r#"Failed to cast primitive numeric type to another numeric type.
The source value is bigger than the target type upper bound.
"#
}
NumberCastCause::Underflow => {
r#"Failed to cast primitive numeric type to another numeric type.
The source value is lesser than the target type lower bound.
"#
}
},
Self::NumericOperation { target, .. } => {
let mut result = String::from(
r#"Failed to perform numeric operation between two primitive types
The result overflows "#,
);
result.push_str(&format!("{target}"));
result.push_str(" bounds.");
return result;
}
Self::RangeCast { .. } => r#"Failed to cast a range to another range type."#,
Self::MalformedRange { .. } => r#"Malformed range bounds."#,
Self::PrimitiveParse { cause, .. } => {
let mut result = String::from(r#"String parse error:"#);
for line in cause.to_string().split("\n") {
result.push_str("\n ");
result.push_str(line);
}
return result;
}
Self::ArityMismatch {
parameters,
arguments,
..
} => match *parameters > *arguments {
true => r#"Not enough arguments."#,
false => r#"Too many arguments."#,
},
Self::UndefinedOperator { .. } => {
r#"The object's type that is responsible to perform specified operation does not
implement this operator."#
}
Self::UnknownField { .. } => r#"The object does not have specified field."#,
Self::FormatError { .. } => r#"Failed to turn the object into string representation."#,
Self::UnknownPackage { .. } => r#"Package lookup failure."#,
Self::Interrupted { .. } => r#"The script explicitly terminated by the host request."#,
Self::StackOverflow { .. } => {
r#"The script engine failed to invoke the function,
because the engine's thread stack exceeded its limit.
This situation may occur in functions with unlimited recursion."#
}
};
String::from(result)
}
fn partition_types(types: &[&'static TypeMeta]) -> Vec<String> {
let mut result = Vec::new();
let type_metas = types.iter().copied().collect::<AHashSet<_>>();
let mut type_families = AHashMap::new();
for ty in type_metas {
let family = ty.family();
if family.is_fn() || family.len() <= 1 {
result.push(format!("'{}'", ty.name()));
continue;
}
match type_families.entry(family) {
Entry::Vacant(entry) => {
let _ = entry.insert(AHashSet::from([ty]));
}
Entry::Occupied(mut entry) => {
let _ = entry.get_mut().insert(ty);
}
}
}
for (family, types) in type_families {
if family.len() == types.len() {
result.push(format!("'{}'", family.name()));
continue;
}
for ty in types {
result.push(format!("'{}'", ty.name()));
}
}
result.sort();
result
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NumberCastCause {
Infinite,
NAN,
Overflow,
Underflow,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NumericOperationKind {
Add,
Sub,
Mul,
Div,
Neg,
Shl,
Shr,
Rem,
}
pub trait NumValue: Debug + Display + Send + Sync + 'static {}
impl<T: Debug + Display + Send + Sync + 'static> NumValue for T {}