use std::{
collections::HashMap,
error,
fmt::{self, Write},
mem,
sync::Arc,
};
use crate::{
exception_private::{ExcType, RawStackFrame},
intern::Interns,
parse::CodeRange,
types::str::StringRepr,
};
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MontyException {
exc_type: ExcType,
message: Option<String>,
traceback: Vec<StackFrame>,
#[serde(default)]
pub(crate) data: ExcData,
}
#[derive(Debug, Clone, Default, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ExcData {
#[default]
None,
Unicode(Box<UnicodeErrorData>),
}
impl ExcData {
#[must_use]
pub fn unicode(&self) -> Option<&UnicodeErrorData> {
match self {
Self::Unicode(data) => Some(data),
Self::None => None,
}
}
#[must_use]
pub(crate) fn estimate_size(&self) -> usize {
match self {
Self::None => 0,
Self::Unicode(data) => data.estimate_size(),
}
}
}
#[derive(Debug, Clone, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct UnicodeErrorData {
pub encoding: String,
pub object: UnicodeErrorObject,
pub start: usize,
pub end: usize,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum UnicodeErrorObject {
Bytes(Vec<u8>),
Str(String),
}
impl UnicodeErrorData {
pub const MAX_OBJECT_LEN: usize = 64 * 1024;
pub(crate) fn encode(encoding: &str, object: &str, start: usize, end: usize, reason: &str) -> ExcData {
if object.len() <= Self::MAX_OBJECT_LEN {
ExcData::Unicode(Box::new(Self {
encoding: encoding.to_owned(),
object: UnicodeErrorObject::Str(object.to_owned()),
start,
end,
reason: reason.to_owned(),
}))
} else {
ExcData::None
}
}
pub(crate) fn decode(encoding: &str, object: &[u8], start: usize, end: usize, reason: &str) -> ExcData {
if object.len() <= Self::MAX_OBJECT_LEN {
ExcData::Unicode(Box::new(Self {
encoding: encoding.to_owned(),
object: UnicodeErrorObject::Bytes(object.to_vec()),
start,
end,
reason: reason.to_owned(),
}))
} else {
ExcData::None
}
}
#[must_use]
pub(crate) fn estimate_size(&self) -> usize {
let object_len = match &self.object {
UnicodeErrorObject::Bytes(b) => b.len(),
UnicodeErrorObject::Str(s) => s.len(),
};
mem::size_of::<Self>() + self.encoding.len() + object_len + self.reason.len()
}
}
const REPEAT_FRAMES_SHOWN: usize = 3;
impl fmt::Display for MontyException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.traceback.is_empty() {
writeln!(f, "Traceback (most recent call last):")?;
}
let mut i = 0;
while i < self.traceback.len() {
let frame = &self.traceback[i];
let mut repeat_count = 1;
while i + repeat_count < self.traceback.len()
&& frames_are_identical(frame, &self.traceback[i + repeat_count])
{
repeat_count += 1;
}
if repeat_count > REPEAT_FRAMES_SHOWN {
for j in 0..REPEAT_FRAMES_SHOWN {
write!(f, "{}", self.traceback[i + j])?;
}
let collapsed = repeat_count - REPEAT_FRAMES_SHOWN;
writeln!(f, " [Previous line repeated {collapsed} more times]")?;
i += repeat_count;
} else {
for j in 0..repeat_count {
write!(f, "{}", self.traceback[i + j])?;
}
i += repeat_count;
}
}
if let Some(msg) = &self.message {
write!(f, "{}: {}", self.exc_type, msg)
} else {
write!(f, "{}", self.exc_type)
}
}
}
impl error::Error for MontyException {}
impl MontyException {
#[must_use]
pub fn new(exc_type: ExcType, message: Option<String>) -> Self {
Self {
exc_type,
message,
traceback: vec![],
data: ExcData::None,
}
}
pub(crate) fn new_full(exc_type: ExcType, message: Option<String>, traceback: Vec<StackFrame>) -> Self {
Self {
exc_type,
message,
traceback,
data: ExcData::None,
}
}
#[must_use]
pub fn with_traceback(exc_type: ExcType, message: Option<String>, traceback: Vec<StackFrame>) -> Self {
Self {
exc_type,
message,
traceback,
data: ExcData::None,
}
}
#[must_use]
pub fn with_data(mut self, data: ExcData) -> Self {
self.data = data;
self
}
#[must_use]
pub fn data(&self) -> &ExcData {
&self.data
}
#[must_use]
pub fn unicode_data(&self) -> Option<&UnicodeErrorData> {
self.data.unicode()
}
#[must_use]
pub fn take_data(&mut self) -> ExcData {
mem::take(&mut self.data)
}
pub fn add_traceback(&mut self, traceback: impl IntoIterator<Item = StackFrame>) {
self.traceback.extend(traceback);
}
pub(crate) fn runtime_error(err: impl fmt::Display) -> Self {
Self {
exc_type: ExcType::RuntimeError,
message: Some(err.to_string()),
traceback: vec![],
data: ExcData::None,
}
}
#[must_use]
pub fn exc_type(&self) -> ExcType {
self.exc_type
}
#[must_use]
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
#[must_use]
pub fn into_message(self) -> Option<String> {
self.message
}
#[must_use]
pub fn traceback(&self) -> &[StackFrame] {
&self.traceback
}
#[must_use]
pub fn summary(&self) -> String {
if let Some(msg) = &self.message {
format!("{}: {}", self.exc_type, msg)
} else {
self.exc_type.to_string()
}
}
#[must_use]
pub fn py_repr(&self) -> String {
let type_str: &'static str = self.exc_type.into();
if let Some(msg) = &self.message {
format!("{}({})", type_str, StringRepr(msg))
} else {
format!("{type_str}()")
}
}
}
fn frames_are_identical(a: &StackFrame, b: &StackFrame) -> bool {
a.filename == b.filename && a.start.line == b.start.line && a.frame_name == b.frame_name
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StackFrame {
pub filename: String,
pub start: CodeLoc,
pub end: CodeLoc,
pub frame_name: Option<String>,
pub preview_line: Option<Arc<str>>,
pub hide_caret: bool,
pub hide_frame_name: bool,
}
impl fmt::Display for StackFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.hide_frame_name {
write!(f, r#" File "{}", line {}"#, self.filename, self.start.line)?;
} else {
write!(f, r#" File "{}", line {}, in "#, self.filename, self.start.line)?;
if let Some(frame_name) = &self.frame_name {
f.write_str(frame_name)?;
} else {
f.write_str("<module>")?;
}
}
if let Some(line) = &self.preview_line {
if self.start.line != self.end.line {
f.write_char('\n')?;
for block_line in line.lines() {
writeln!(f, " {block_line}")?;
}
return Ok(());
}
let trimmed = line.trim_start();
writeln!(f, "\n {trimmed}")?;
if !self.hide_caret {
let leading_spaces = line.len() - trimmed.len();
let caret_start = if self.start.column as usize > leading_spaces {
4 + self.start.column as usize - leading_spaces - 1
} else {
4
};
f.write_str(&" ".repeat(caret_start))?;
let caret_len = (self.end.column - self.start.column).max(1) as usize;
writeln!(f, "{}", "~".repeat(caret_len))?;
}
} else {
f.write_char('\n')?;
}
Ok(())
}
}
impl StackFrame {
pub(crate) fn from_raw(f: &RawStackFrame, interns: &Interns, source_map: &mut SourceMap<'_>) -> Self {
let filename = interns.get_str(f.position.filename).to_string();
let (start, end, preview_line) = source_map.resolve_range(f.position);
Self {
filename,
start,
end,
frame_name: f.frame_name.map(|id| interns.get_str(id).to_string()),
preview_line,
hide_caret: f.hide_caret,
hide_frame_name: false,
}
}
pub(crate) fn from_position_syntax_error(
position: CodeRange,
filename: &str,
source_map: &mut SourceMap<'_>,
) -> Self {
let (start, end, preview_line) = source_map.resolve_range(position);
Self {
filename: filename.to_string(),
start,
end,
frame_name: None,
preview_line,
hide_caret: false,
hide_frame_name: true,
}
}
pub(crate) fn from_position(position: CodeRange, filename: &str, source_map: &mut SourceMap<'_>) -> Self {
let (start, end, preview_line) = source_map.resolve_range(position);
Self {
filename: filename.to_string(),
start,
end,
frame_name: None,
preview_line,
hide_caret: false,
hide_frame_name: false,
}
}
pub(crate) fn from_position_no_caret(position: CodeRange, filename: &str, source_map: &mut SourceMap<'_>) -> Self {
let (start, end, preview_line) = source_map.resolve_range(position);
Self {
filename: filename.to_string(),
start,
end,
frame_name: None,
preview_line,
hide_caret: true,
hide_frame_name: false,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CodeLoc {
pub line: u32,
pub column: u32,
}
impl Default for CodeLoc {
fn default() -> Self {
Self { line: 1, column: 1 }
}
}
impl CodeLoc {
#[must_use]
pub fn new(line: u32, column: u32) -> Self {
Self {
line: line.saturating_add(1),
column: column.saturating_add(1),
}
}
}
pub struct SourceMap<'s> {
source: &'s str,
line_starts: Vec<u32>,
line_cache: HashMap<usize, Arc<str>>,
}
impl<'s> SourceMap<'s> {
#[must_use]
pub fn new(source: &'s str) -> Self {
let mut line_starts = Vec::with_capacity(source.len() / 40 + 1);
line_starts.push(0);
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
let start = u32::try_from(i + 1).unwrap_or(u32::MAX);
line_starts.push(start);
}
}
Self {
source,
line_starts,
line_cache: HashMap::new(),
}
}
pub(crate) fn resolve_range(&mut self, range: CodeRange) -> (CodeLoc, CodeLoc, Option<Arc<str>>) {
let (start_line_idx, start) = self.resolve_byte(range.start_byte);
let (end_line_idx, end) = self.resolve_byte(range.end_byte);
let preview_line = if start_line_idx == end_line_idx {
let line_text = self.line_text(start_line_idx);
Some(Arc::clone(
self.line_cache
.entry(start_line_idx)
.or_insert_with(|| Arc::from(line_text)),
))
} else {
Some(Arc::from(self.multiline_preview(start_line_idx, end_line_idx)))
};
(start, end, preview_line)
}
fn multiline_preview(&self, start_line_idx: usize, end_line_idx: usize) -> String {
let total = end_line_idx - start_line_idx + 1;
let displayed: Vec<&str> = if total <= 3 {
(start_line_idx..=end_line_idx).map(|i| self.line_text(i)).collect()
} else {
vec![self.line_text(start_line_idx), self.line_text(end_line_idx)]
};
let dedent = displayed
.iter()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
let stripped = |line: &str| line.get(dedent..).unwrap_or("").to_owned();
if total <= 3 {
displayed
.iter()
.map(|line| stripped(line))
.collect::<Vec<_>>()
.join("\n")
} else {
format!(
"{}\n...<{} lines>...\n{}",
stripped(displayed[0]),
total - 2,
stripped(displayed[1])
)
}
}
fn resolve_byte(&self, byte: u32) -> (usize, CodeLoc) {
let line_idx = self.line_starts.partition_point(|&s| s <= byte).saturating_sub(1);
let line_start = self.line_starts[line_idx];
let slice_start = line_start as usize;
let slice_end = (byte as usize).min(self.source.len());
let slice = &self.source[slice_start..slice_end];
let col = if slice.is_ascii() {
u32::try_from(slice.len()).unwrap_or(u32::MAX)
} else {
u32::try_from(slice.chars().count()).unwrap_or(u32::MAX)
};
(
line_idx,
CodeLoc::new(u32::try_from(line_idx).expect("line number exceeds u32"), col),
)
}
fn line_text(&self, line_idx: usize) -> &'s str {
let start = self.line_starts[line_idx] as usize;
let end = self
.line_starts
.get(line_idx + 1)
.map_or(self.source.len(), |&next| next.saturating_sub(1) as usize);
let end = end.max(start);
let line = &self.source[start..end];
line.strip_suffix('\r').unwrap_or(line)
}
}