logicaffeine_base/
span.rs

1//! Source location tracking for error reporting.
2//!
3//! A [`Span`] represents a contiguous region of source text using byte offsets.
4//! Every token, expression, and error in logicaffeine carries a span, enabling
5//! precise error messages that point to the exact location of problems.
6//!
7//! # Byte Offsets
8//!
9//! Spans use byte offsets, not character indices. This matches Rust's string
10//! slicing semantics: `&source[span.start..span.end]` extracts the spanned text.
11//!
12//! # Example
13//!
14//! ```
15//! use logicaffeine_base::Span;
16//!
17//! let source = "hello world";
18//! let span = Span::new(0, 5);
19//!
20//! assert_eq!(&source[span.start..span.end], "hello");
21//! assert_eq!(span.len(), 5);
22//! ```
23
24/// A byte-offset range in source text.
25///
26/// Spans are `Copy` and cheap to pass around. Use [`Span::merge`] to combine
27/// spans when building compound expressions.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub struct Span {
30    /// Byte offset of the first character (inclusive).
31    pub start: usize,
32    /// Byte offset past the last character (exclusive).
33    pub end: usize,
34}
35
36impl Span {
37    /// Creates a span from byte offsets.
38    ///
39    /// No validation is performed; `start` may exceed `end`.
40    pub fn new(start: usize, end: usize) -> Self {
41        Self { start, end }
42    }
43
44    /// Creates a span covering from the start of `self` to the end of `other`.
45    ///
46    /// Useful for building compound expressions: the span of `a + b` is
47    /// `a.span.merge(b.span)`.
48    pub fn merge(self, other: Span) -> Span {
49        Span {
50            start: self.start.min(other.start),
51            end: self.end.max(other.end),
52        }
53    }
54
55    /// Returns the length of the span in bytes.
56    pub fn len(&self) -> usize {
57        self.end.saturating_sub(self.start)
58    }
59
60    /// Returns `true` if this span covers no bytes.
61    pub fn is_empty(&self) -> bool {
62        self.start >= self.end
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn span_new_stores_positions() {
72        let span = Span::new(5, 10);
73        assert_eq!(span.start, 5);
74        assert_eq!(span.end, 10);
75    }
76
77    #[test]
78    fn span_default_is_zero() {
79        let span = Span::default();
80        assert_eq!(span.start, 0);
81        assert_eq!(span.end, 0);
82    }
83
84    #[test]
85    fn span_merge_combines_ranges() {
86        let a = Span::new(5, 10);
87        let b = Span::new(8, 15);
88        let merged = a.merge(b);
89        assert_eq!(merged.start, 5);
90        assert_eq!(merged.end, 15);
91    }
92
93    #[test]
94    fn span_len_returns_size() {
95        let span = Span::new(5, 10);
96        assert_eq!(span.len(), 5);
97    }
98
99    #[test]
100    fn span_is_empty_for_zero_length() {
101        let empty = Span::new(5, 5);
102        assert!(empty.is_empty());
103
104        let nonempty = Span::new(5, 10);
105        assert!(!nonempty.is_empty());
106    }
107}