horkos 0.2.0

Cloud infrastructure language where insecure code won't compile
Documentation
//! Source location tracking for error messages.
//!
//! Every AST node should be wrapped in `Spanned<T>` to enable
//! precise error messages with source code snippets.

use serde::{Deserialize, Serialize};

/// A span of source code.
///
/// Tracks both byte offsets (for slicing source) and line/column
/// (for display in error messages).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Span {
    /// Byte offset of start (0-indexed)
    pub start: u32,
    /// Byte offset of end (exclusive)
    pub end: u32,
    /// Line number of start (1-indexed, for display)
    pub start_line: usize,
    /// Column number of start (1-indexed, for display)
    pub start_col: usize,
}

impl Span {
    /// Create a new span.
    pub fn new(start: u32, end: u32, start_line: usize, start_col: usize) -> Self {
        Self {
            start,
            end,
            start_line,
            start_col,
        }
    }

    /// Create a span covering a single position.
    pub fn point(offset: u32, line: usize, col: usize) -> Self {
        Self::new(offset, offset + 1, line, col)
    }

    /// Create a dummy span for generated/synthetic nodes.
    pub fn dummy() -> Self {
        Self::new(0, 0, 0, 0)
    }

    /// Check if this is a dummy span.
    pub fn is_dummy(&self) -> bool {
        self.start == 0 && self.end == 0 && self.start_line == 0
    }

    /// Merge two spans, returning a span covering both.
    pub fn merge(self, other: Span) -> Span {
        // If either is dummy, return the other
        if self.is_dummy() {
            return other;
        }
        if other.is_dummy() {
            return self;
        }

        let (start, start_line, start_col) = if self.start <= other.start {
            (self.start, self.start_line, self.start_col)
        } else {
            (other.start, other.start_line, other.start_col)
        };

        Span {
            start,
            end: self.end.max(other.end),
            start_line,
            start_col,
        }
    }

    /// Get the length in bytes.
    pub fn len(&self) -> u32 {
        self.end.saturating_sub(self.start)
    }

    /// Check if the span is empty.
    pub fn is_empty(&self) -> bool {
        self.start >= self.end
    }

    /// Extract the source text covered by this span.
    pub fn slice<'a>(&self, source: &'a str) -> &'a str {
        &source[self.start as usize..self.end as usize]
    }

    /// Convert to a range for ariadne.
    pub fn to_range(&self) -> std::ops::Range<usize> {
        self.start as usize..self.end as usize
    }
}

impl Default for Span {
    fn default() -> Self {
        Self::dummy()
    }
}

impl std::fmt::Display for Span {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}", self.start_line, self.start_col)
    }
}

/// A value paired with its source location.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Spanned<T> {
    pub node: T,
    pub span: Span,
}

impl<T> Spanned<T> {
    /// Create a new spanned value.
    pub fn new(node: T, span: Span) -> Self {
        Self { node, span }
    }

    /// Create a spanned value with a dummy span.
    pub fn dummy(node: T) -> Self {
        Self::new(node, Span::dummy())
    }

    /// Map the inner value, preserving the span.
    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Spanned<U> {
        Spanned {
            node: f(self.node),
            span: self.span,
        }
    }

    /// Get a reference to the inner value.
    pub fn as_ref(&self) -> Spanned<&T> {
        Spanned {
            node: &self.node,
            span: self.span,
        }
    }
}

impl<T: PartialEq> PartialEq for Spanned<T> {
    fn eq(&self, other: &Self) -> bool {
        // Compare only the node, not the span
        self.node == other.node
    }
}

impl<T: Eq> Eq for Spanned<T> {}

impl<T: std::hash::Hash> std::hash::Hash for Spanned<T> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        // Hash only the node, not the span
        self.node.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_span_merge() {
        let a = Span::new(0, 5, 1, 1);
        let b = Span::new(3, 10, 1, 4);
        let merged = a.merge(b);
        assert_eq!(merged.start, 0);
        assert_eq!(merged.end, 10);
    }

    #[test]
    fn test_span_merge_with_dummy() {
        let a = Span::new(5, 10, 2, 3);
        let dummy = Span::dummy();
        assert_eq!(a.merge(dummy), a);
        assert_eq!(dummy.merge(a), a);
    }

    #[test]
    fn test_span_slice() {
        let source = "hello world";
        let span = Span::new(0, 5, 1, 1);
        assert_eq!(span.slice(source), "hello");
    }

    #[test]
    fn test_spanned_equality_ignores_span() {
        let a = Spanned::new(42, Span::new(0, 1, 1, 1));
        let b = Spanned::new(42, Span::new(100, 101, 5, 5));
        assert_eq!(a, b);
    }
}