erg_common 0.4.2

A common components library of Erg
Documentation
//! provides common components for error handling.
//!
//! エラー処理に関する汎用的なコンポーネントを提供する
use std::cmp;
use std::fmt;
use std::fmt::Write as _;
use std::io::{stderr, BufWriter, Write as _};

use crate::color::*;
use crate::config::Input;
use crate::traits::{Locational, Stream};
use crate::Str;
use crate::{fmt_option, impl_display_from_debug, switch_lang};

/// ErrorKindと言っているが、ErrorだけでなくWarning, Exceptionも含まれる
/// Numbering of this is not specifically related to ErrFmt.errno().
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ErrorKind {
    /* compile errors */
    AssignError = 0,
    AttributeError,
    BytecodeError,
    CompilerSystemError,
    EnvironmentError,
    FeatureError,
    ImportError,
    IndentationError,
    NameError,
    NotImplementedError,
    PatternError,
    SyntaxError,
    TabError,
    TypeError,
    UnboundLocalError,
    PurityError,
    HasEffect,
    MoveError,
    NotConstExpr,
    InheritanceError,
    VisibilityError,
    DummyError,
    /* compile warnings */
    AttributeWarning = 60,
    CastWarning,
    DeprecationWarning,
    FutureWarning,
    ImportWarning,
    PendingDeprecationWarning,
    SyntaxWarning,
    TypeWarning,
    NameWarning,
    UnusedWarning,
    Warning,
    /* runtime errors */
    ArithmeticError = 100,
    AssertionError,
    BlockingIOError,
    BrokenPipeError,
    BufferError,
    ChildProcessError,
    ConnectionAbortedError,
    ConnectionError,
    ConnectionRefusedError,
    ConnectionResetError,
    EOFError,
    FileExistsError,
    FileNotFoundError,
    IndexError,
    InterruptedError,
    IoError,
    IsADirectoryError,
    KeyError,
    LookupError,
    MemoryError,
    ModuleNotFoundError,
    NotADirectoryError,
    OSError,
    OverflowError,
    PermissionError,
    ProcessLookupError,
    RecursionError,
    ReferenceError,
    RuntimeAttributeError,
    RuntimeError,
    RuntimeTypeError,
    RuntimeUnicodeError,
    TimeoutError,
    UnicodeError,
    UserError,
    ValueError,
    VMSystemError,
    WindowsError,
    ZeroDivisionError,
    /* runtime warnings */
    BytesWarning = 180,
    ResourceWarning,
    RuntimeWarning,
    UnicodeWarning,
    UserWarning,
    /* exceptions */
    BaseException = 200,
    Exception,
    GeneratorExit,
    KeyboardInterrupt,
    StopAsyncIteration,
    StopIteration,
    SystemExit,
    UserException,
}

use ErrorKind::*;

impl_display_from_debug!(ErrorKind);

impl From<&str> for ErrorKind {
    fn from(s: &str) -> ErrorKind {
        match s {
            "AssignError" => Self::AssignError,
            "AttributeError" => Self::AttributeError,
            "BytecodeError" => Self::BytecodeError,
            "CompilerSystemError" => Self::CompilerSystemError,
            "EnvironmentError" => Self::EnvironmentError,
            "FeatureError" => Self::FeatureError,
            "ImportError" => Self::ImportError,
            "IndentationError" => Self::IndentationError,
            "NameError" => Self::NameError,
            "NotImplementedError" => Self::NotImplementedError,
            "PatternError" => Self::PatternError,
            "SyntaxError" => Self::SyntaxError,
            "TabError" => Self::TabError,
            "TypeError" => Self::TypeError,
            "UnboundLocalError" => Self::UnboundLocalError,
            "HasEffect" => Self::HasEffect,
            "PurityError" => Self::PurityError,
            "MoveError" => Self::MoveError,
            "AttributeWarning" => Self::AttributeWarning,
            "CastWarning" => Self::CastWarning,
            "DeprecationWarning" => Self::DeprecationWarning,
            "FutureWarning" => Self::FutureWarning,
            "ImportWarning" => Self::ImportWarning,
            "PendingDeprecationWarning" => Self::PendingDeprecationWarning,
            "SyntaxWarning" => Self::SyntaxWarning,
            "TypeWarning" => Self::TypeWarning,
            "NameWarning" => Self::NameWarning,
            "UnusedWarning" => Self::UnusedWarning,
            "Warning" => Self::Warning,
            "ArithmeticError" => Self::ArithmeticError,
            "AssertionError" => Self::AssertionError,
            "BlockingIOError" => Self::BlockingIOError,
            "BrokenPipeError" => Self::BrokenPipeError,
            "BufferError" => Self::BufferError,
            "ChildProcessError" => Self::ChildProcessError,
            "ConnectionAbortedError" => Self::ConnectionAbortedError,
            "ConnectionError" => Self::ConnectionError,
            "ConnectionRefusedError" => Self::ConnectionRefusedError,
            "ConnectionResetError" => Self::ConnectionResetError,
            "EOFError" => Self::EOFError,
            "FileExistsError" => Self::FileExistsError,
            "FileNotFoundError" => Self::FileNotFoundError,
            "IndexError" => Self::IndexError,
            "InterruptedError" => Self::InterruptedError,
            "IoError" => Self::IoError,
            "IsADirectoryError" => Self::IsADirectoryError,
            "KeyError" => Self::KeyError,
            "LookupError" => Self::LookupError,
            "MemoryError" => Self::MemoryError,
            "ModuleNotFoundError" => Self::ModuleNotFoundError,
            "NotADirectoryError" => Self::NotADirectoryError,
            "OSError" => Self::OSError,
            "OverflowError" => Self::OverflowError,
            "PermissionError" => Self::PermissionError,
            "ProcessLookupError" => Self::ProcessLookupError,
            "RecursionError" => Self::RecursionError,
            "ReferenceError" => Self::ReferenceError,
            "RuntimeAttributeError" => Self::RuntimeAttributeError,
            "RuntimeError" => Self::RuntimeError,
            "RuntimeTypeError" => Self::RuntimeTypeError,
            "RuntimeUnicodeError" => Self::RuntimeUnicodeError,
            "TimeoutError" => Self::TimeoutError,
            "UnicodeError" => Self::UnicodeError,
            "UserError" => Self::UserError,
            "ValueError" => Self::ValueError,
            "VMSystemError" => Self::VMSystemError,
            "WindowsError" => Self::WindowsError,
            "ZeroDivisionError" => Self::ZeroDivisionError,
            "BytesWarning" => Self::BytesWarning,
            "ResourceWarning" => Self::ResourceWarning,
            "RuntimeWarning" => Self::RuntimeWarning,
            "UnicodeWarning" => Self::UnicodeWarning,
            "UserWarning" => Self::UserWarning,
            "BaseException" => Self::BaseException,
            "Exception" => Self::Exception,
            "GeneratorExit" => Self::GeneratorExit,
            "KeyboardInterrupt" => Self::KeyboardInterrupt,
            "StopAsyncIteration" => Self::StopAsyncIteration,
            "StopIteration" => Self::StopIteration,
            "SystemExit" => Self::SystemExit,
            "UserException" => Self::UserException,
            _ => Self::UserError,
        }
    }
}

/// points the location (of an error) in a code
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Location {
    RangePair {
        ln_first: (usize, usize),
        col_first: (usize, usize),
        ln_second: (usize, usize),
        col_second: (usize, usize),
    },
    Range {
        ln_begin: usize,
        col_begin: usize,
        ln_end: usize,
        col_end: usize,
    },
    LineRange(usize, usize),
    Line(usize),
    Unknown,
}

impl Location {
    pub fn concat<L: Locational, R: Locational>(l: &L, r: &R) -> Self {
        match (l.ln_begin(), l.col_begin(), r.ln_end(), r.col_end()) {
            (Some(lb), Some(cb), Some(le), Some(ce)) => Self::range(lb, cb, le, ce),
            (Some(lb), _, Some(le), _) => Self::LineRange(lb, le),
            (Some(l), _, _, _) | (_, _, Some(l), _) => Self::Line(l),
            _ => Self::Unknown,
        }
    }

    pub const fn range(ln_begin: usize, col_begin: usize, ln_end: usize, col_end: usize) -> Self {
        Self::Range {
            ln_begin,
            col_begin,
            ln_end,
            col_end,
        }
    }

    pub fn pair(lhs: Self, rhs: Self) -> Self {
        Self::RangePair {
            ln_first: (lhs.ln_begin().unwrap(), lhs.ln_end().unwrap()),
            col_first: (lhs.col_begin().unwrap(), lhs.col_end().unwrap()),
            ln_second: (rhs.ln_begin().unwrap(), rhs.ln_end().unwrap()),
            col_second: (rhs.col_begin().unwrap(), rhs.col_end().unwrap()),
        }
    }

    pub const fn ln_begin(&self) -> Option<usize> {
        match self {
            Self::RangePair {
                ln_first: (ln_begin, _),
                ..
            }
            | Self::Range { ln_begin, .. }
            | Self::LineRange(ln_begin, _)
            | Self::Line(ln_begin) => Some(*ln_begin),
            Self::Unknown => None,
        }
    }

    pub const fn ln_end(&self) -> Option<usize> {
        match self {
            Self::RangePair {
                ln_second: (_, ln_end),
                ..
            }
            | Self::Range { ln_end, .. }
            | Self::LineRange(ln_end, _)
            | Self::Line(ln_end) => Some(*ln_end),
            Self::Unknown => None,
        }
    }

    pub const fn col_begin(&self) -> Option<usize> {
        match self {
            Self::RangePair {
                col_first: (col_begin, _),
                ..
            }
            | Self::Range { col_begin, .. } => Some(*col_begin),
            _ => None,
        }
    }

    pub const fn col_end(&self) -> Option<usize> {
        match self {
            Self::RangePair {
                col_second: (_, col_end),
                ..
            }
            | Self::Range { col_end, .. } => Some(*col_end),
            _ => None,
        }
    }
}

/// Erg内で使われるエラーの共通部分
/// 使用する場合は必ずwrapすること
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ErrorCore {
    pub errno: usize,
    pub kind: ErrorKind,
    pub loc: Location,
    pub desc: Str,
    pub hint: Option<Str>,
}

impl ErrorCore {
    pub fn new<S: Into<Str>>(
        errno: usize,
        kind: ErrorKind,
        loc: Location,
        desc: S,
        hint: Option<Str>,
    ) -> Self {
        Self {
            errno,
            kind,
            loc,
            desc: desc.into(),
            hint,
        }
    }

    pub fn dummy(errno: usize) -> Self {
        Self::new(
            errno,
            DummyError,
            Location::Line(errno as usize),
            "<dummy>",
            None,
        )
    }

    pub fn unreachable(fn_name: &str, line: u32) -> Self {
        Self::bug(line as usize, Location::Line(line as usize), fn_name, line)
    }

    pub fn bug(errno: usize, loc: Location, fn_name: &str, line: u32) -> Self {
        Self::new(
            errno,
            CompilerSystemError,
            loc,
            switch_lang!(
                "japanese" => format!("これはErgのバグです、開発者に報告して下さい (https://github.com/erg-lang/erg)\n{fn_name}:{line}より発生"),
                "simplified_chinese" => format!("这是Erg的bug,请报告给https://github.com/erg-lang/erg\n原因来自:{fn_name}:{line}"),
                "traditional_chinese" => format!("这是Erg的bug,请报告给https://github.com/erg-lang/erg\n原因来自:{fn_name}:{line}"),
                "english" => format!("this is a bug of Erg, please report it to https://github.com/erg-lang/erg\ncaused from: {fn_name}:{line}"),
            ),
            None,
        )
    }
}

pub const VBAR_UNICODE: &str = "";
pub const VBAR_BREAK_UNICODE: &str = "·";

fn format_code_and_pointer<E: ErrorDisplay + ?Sized>(
    e: &E,
    ln_begin: usize,
    ln_end: usize,
    col_begin: usize,
    col_end: usize,
) -> String {
    let codes = if e.input().is_repl() {
        vec![e.input().reread()]
    } else {
        e.input().reread_lines(ln_begin, ln_end)
    };
    let mut res = CYAN.to_string();
    let final_step = ln_end - ln_begin;
    for (i, lineno) in (ln_begin..=ln_end).enumerate() {
        let mut pointer = " ".repeat(lineno.to_string().len() + 2); // +2 means `| `
        if i == 0 && i == final_step {
            pointer += &" ".repeat(col_begin);
            pointer += &"^".repeat(cmp::max(1, col_end - col_begin));
        } else if i == 0 {
            pointer += &" ".repeat(col_begin);
            pointer += &"^".repeat(cmp::max(1, codes[i].len() - col_begin));
        } else if i == final_step {
            pointer += &"^".repeat(col_end);
        } else {
            pointer += &"^".repeat(cmp::max(1, codes[i].len()));
        }
        writeln!(
            res,
            "{lineno}{VBAR_UNICODE} {code}\n{pointer}",
            code = codes[i]
        )
        .unwrap();
    }
    res + RESET
}

/// format:
/// ```console
/// Error[#{.errno}]: File {file}, line {.loc (as line)}, in {.caused_by}
/// {.loc (as line)}| {src}
/// {pointer}
/// {.kind}: {.desc}
/// ```
///
/// example:
/// ```console
/// Error[#12]: File <stdin>, line 1, in <module>
/// 1| 100 = i
///    ^^^
/// SyntaxError: cannot assign to 100
/// ```
pub trait ErrorDisplay {
    fn core(&self) -> &ErrorCore;
    fn input(&self) -> &Input;
    /// The block name the error caused.
    /// This will be None if the error occurred before semantic analysis.
    /// As for the internal error, do not put the fn name here.
    fn caused_by(&self) -> &str;
    /// the previous error that caused this error.
    fn ref_inner(&self) -> Option<&Self>;

    fn write_to_stderr(&self) {
        let mut writer = BufWriter::new(stderr());
        writer
            .write_all(
                format!(
                    "{}{}{}: {}{}\n",
                    self.format_header(),
                    self.format_code_and_pointer(),
                    self.core().kind,
                    self.core().desc,
                    fmt_option!(pre format!("\n{GREEN}hint{RESET}: "), self.core().hint),
                )
                .as_bytes(),
            )
            .unwrap();
        writer.flush().unwrap();
        if let Some(inner) = self.ref_inner() {
            inner.write_to_stderr()
        }
    }

    /// fmt::Display実装用
    fn format(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(
            f,
            "{}{}{}: {}{}",
            self.format_header(),
            self.format_code_and_pointer(),
            self.core().kind,
            self.core().desc,
            fmt_option!(pre format!("\n{GREEN}hint{RESET}: "), self.core().hint),
        )?;
        if let Some(inner) = self.ref_inner() {
            inner.format(f)
        } else {
            Ok(())
        }
    }

    fn format_header(&self) -> String {
        let kind = self.core().kind as u8;
        let (color, err_or_warn) = if kind < 100 {
            (RED, "Error")
        } else if (100..150).contains(&kind) {
            (YELLOW, "Warning")
        } else if (150..200).contains(&kind) {
            (DEEP_RED, "Error")
        } else {
            ("", "Exception")
        };
        let loc = match self.core().loc {
            Location::Range {
                ln_begin, ln_end, ..
            } if ln_begin == ln_end => format!(", line {ln_begin}"),
            Location::Range {
                ln_begin, ln_end, ..
            }
            | Location::LineRange(ln_begin, ln_end) => format!(", line {ln_begin}..{ln_end}"),
            Location::RangePair {
                ln_first: (l1, l2),
                ln_second: (l3, l4),
                ..
            } => format!(", line {l1}..{l2}, {l3}..{l4}"),
            Location::Line(lineno) => format!(", line {lineno}"),
            Location::Unknown => "".to_string(),
        };
        let caused_by = if self.caused_by() != "" {
            format!(", in {}", self.caused_by())
        } else {
            "".to_string()
        };
        format!(
            "{color}{err_or_warn}[#{errno:>04}]{RESET}: File {input}{loc}{caused_by}\n",
            errno = self.core().errno,
            input = self.input().enclosed_name(),
        )
    }

    fn format_code_and_pointer(&self) -> String {
        match self.core().loc {
            Location::RangePair {
                ln_first,
                col_first,
                ln_second,
                col_second,
            } => {
                format_code_and_pointer(self, ln_first.0, ln_first.1, col_first.0, col_first.1)
                    + &format_code_and_pointer(
                        self,
                        ln_second.0,
                        ln_second.1,
                        col_second.0,
                        col_second.1,
                    )
            }
            Location::Range {
                ln_begin,
                col_begin,
                ln_end,
                col_end,
            } => format_code_and_pointer(self, ln_begin, ln_end, col_begin, col_end),
            Location::LineRange(ln_begin, ln_end) => {
                let codes = if self.input().is_repl() {
                    vec![self.input().reread()]
                } else {
                    self.input().reread_lines(ln_begin, ln_end)
                };
                let mut res = CYAN.to_string();
                for (i, lineno) in (ln_begin..=ln_end).enumerate() {
                    let mut pointer = " ".repeat(lineno.to_string().len() + 2); // +2 means `| `
                    pointer += &"^".repeat(cmp::max(1, codes[i].len()));
                    writeln!(
                        res,
                        "{lineno}{VBAR_UNICODE} {code}\n{pointer}",
                        code = codes[i]
                    )
                    .unwrap();
                }
                res + RESET
            }
            Location::Line(lineno) => {
                let code = if self.input().is_repl() {
                    self.input().reread()
                } else {
                    self.input().reread_lines(lineno, lineno).remove(0)
                };
                format!("{CYAN}{lineno}{VBAR_UNICODE} {code}\n{RESET}")
            }
            Location::Unknown => match self.input() {
                Input::File(_) => "\n".to_string(),
                other => format!(
                    "{CYAN}?{VBAR_UNICODE} {code}\n{RESET}",
                    code = other.reread()
                ),
            },
        }
    }
}

pub trait MultiErrorDisplay<Item: ErrorDisplay>: Stream<Item> {
    fn fmt_all_stderr(&self) {
        for err in self.iter() {
            err.write_to_stderr();
        }
    }

    fn fmt_all(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for err in self.iter() {
            err.format(f)?;
        }
        write!(f, "")
    }
}