rockscript 0.2.0

Esolang called Rockscript
use std::ops;

use ariadne::{Color, Label, Report, Source};

#[derive(Debug)]
pub struct LexError(ops::Range<usize>);

impl From<std::ops::Range<usize>> for LexError {
    fn from(value: std::ops::Range<usize>) -> Self {
        LexError(value)
    }
}

pub struct ParseError {
    pub desc: String,
    pub span: ops::Range<usize>,
}

pub struct RuntimeError {
    pub desc: String,
    pub span: ops::Range<usize>,
}

pub trait Diagnostic {
    fn span(&self) -> std::ops::Range<usize>;
    fn desc(&self) -> String;
}

impl Diagnostic for LexError {
    fn span(&self) -> std::ops::Range<usize> { self.0.clone() }
    fn desc(&self) -> String { "unexpected character(s)".to_string() }
}

impl Diagnostic for ParseError {
    fn span(&self) -> std::ops::Range<usize> { self.span.clone() }
    fn desc(&self) -> String { self.desc.clone() }
}

impl Diagnostic for RuntimeError {
    fn span(&self) -> std::ops::Range<usize> { self.span.clone() }
    fn desc(&self) -> String { self.desc.clone() }
}

fn byte_to_char(source: &str, byte: usize) -> usize {
    source[..byte].chars().count()
}

pub fn report_error(source: &str, filename: &str, error: Box<dyn Diagnostic>) {
    let char_span = 
        byte_to_char(source, error.span().start)..byte_to_char(source, error.span().end);

    Report::build(
        ariadne::ReportKind::Error, (filename, char_span.clone())
    )
        .with_message(error.desc())
        .with_label(
            Label::new((filename, char_span))
                .with_message("here")
                .with_color(Color::Red)
        )
        .finish()
        .eprint((filename, Source::from(source)))
        .unwrap();
}