aeri 0.2.1

Aeri is a Cardano smart contract language by Trevor Knott and Knott Dynamics, with tools for compiling contracts to UPLC.
Documentation
use std::io;

use thiserror::Error;

pub type Result<T> = std::result::Result<T, AeriError>;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Span {
    pub start: usize,
    pub end: usize,
    pub line: usize,
    pub column: usize,
}

impl Span {
    pub fn new(start: usize, end: usize, line: usize, column: usize) -> Self {
        Self {
            start,
            end,
            line,
            column,
        }
    }

    pub fn join(self, other: Span) -> Self {
        Self {
            start: self.start.min(other.start),
            end: self.end.max(other.end),
            line: self.line,
            column: self.column,
        }
    }
}

#[derive(Debug, Error)]
pub enum AeriError {
    #[error("{file}:{line}:{column}: {message}{snippet}")]
    Diagnostic {
        file: String,
        line: usize,
        column: usize,
        length: usize,
        message: String,
        snippet: String,
    },
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
    Json(#[from] serde_json::Error),
}

impl AeriError {
    pub fn at(
        file: impl Into<String>,
        line: usize,
        column: usize,
        message: impl Into<String>,
    ) -> Self {
        Self::Diagnostic {
            file: file.into(),
            line,
            column,
            length: 1,
            message: message.into(),
            snippet: String::new(),
        }
    }

    pub fn at_span(file: impl Into<String>, span: Span, message: impl Into<String>) -> Self {
        Self::Diagnostic {
            file: file.into(),
            line: span.line,
            column: span.column,
            length: span.end.saturating_sub(span.start).max(1),
            message: message.into(),
            snippet: String::new(),
        }
    }

    pub fn with_source(self, source: &str) -> Self {
        let Self::Diagnostic {
            file,
            line,
            column,
            length,
            message,
            snippet,
        } = self
        else {
            return self;
        };

        if !snippet.is_empty() {
            return Self::Diagnostic {
                file,
                line,
                column,
                length,
                message,
                snippet,
            };
        }

        let source_line = source.lines().nth(line.saturating_sub(1)).unwrap_or("");
        let caret_width = length
            .min(source_line.len().saturating_sub(column.saturating_sub(1)))
            .max(1);
        let marker = format!(
            "{}{}",
            " ".repeat(column.saturating_sub(1)),
            "^".repeat(caret_width)
        );

        Self::Diagnostic {
            file,
            line,
            column,
            length,
            message,
            snippet: format!("\n  {source_line}\n  {marker}"),
        }
    }
}