Skip to main content

tatara_lisp/
span.rs

1//! Source position tracking for the spanned reader/expander pipeline.
2//!
3//! A `Span` records a half-open byte range `[start, end)` into the original
4//! source string the reader was given. Spans are not portable across source
5//! inputs — they are meaningful only relative to the string that produced
6//! them, which the caller is responsible for holding onto.
7//!
8//! Nodes produced by macro expansion (not present in the user's source)
9//! carry `Span::synthetic()`, which is still a valid `Span` but compares
10//! unequal to any real source span.
11
12use std::fmt;
13
14/// A half-open byte range `[start, end)` into the source.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub struct Span {
17    pub start: usize,
18    pub end: usize,
19}
20
21impl Span {
22    pub const fn new(start: usize, end: usize) -> Self {
23        Self { start, end }
24    }
25
26    /// A span for nodes produced by macro expansion that have no direct
27    /// source origin. `start == end == usize::MAX` is the sentinel.
28    pub const fn synthetic() -> Self {
29        Self {
30            start: usize::MAX,
31            end: usize::MAX,
32        }
33    }
34
35    pub const fn is_synthetic(&self) -> bool {
36        self.start == usize::MAX && self.end == usize::MAX
37    }
38
39    /// Merge two spans into the smallest span that covers both.
40    /// If either is synthetic, returns the other; if both are synthetic,
41    /// returns synthetic.
42    pub fn merge(self, other: Span) -> Span {
43        if self.is_synthetic() {
44            return other;
45        }
46        if other.is_synthetic() {
47            return self;
48        }
49        Span {
50            start: self.start.min(other.start),
51            end: self.end.max(other.end),
52        }
53    }
54
55    /// Resolve `(line, column)` for `byte_offset` in `src` (1-indexed).
56    /// Used for human-readable error messages; O(n) in source length — not
57    /// for hot paths.
58    pub fn line_col(src: &str, byte_offset: usize) -> (usize, usize) {
59        let mut line = 1usize;
60        let mut col = 1usize;
61        for (i, ch) in src.char_indices() {
62            if i >= byte_offset {
63                return (line, col);
64            }
65            if ch == '\n' {
66                line += 1;
67                col = 1;
68            } else {
69                col += 1;
70            }
71        }
72        (line, col)
73    }
74}
75
76impl fmt::Display for Span {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        if self.is_synthetic() {
79            f.write_str("<synthetic>")
80        } else {
81            write!(f, "{}..{}", self.start, self.end)
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn merge_real_spans() {
92        let a = Span::new(5, 10);
93        let b = Span::new(8, 20);
94        assert_eq!(a.merge(b), Span::new(5, 20));
95        assert_eq!(b.merge(a), Span::new(5, 20));
96    }
97
98    #[test]
99    fn merge_with_synthetic_preserves_real() {
100        let a = Span::new(5, 10);
101        assert_eq!(a.merge(Span::synthetic()), a);
102        assert_eq!(Span::synthetic().merge(a), a);
103    }
104
105    #[test]
106    fn merge_two_synthetics_is_synthetic() {
107        let s = Span::synthetic();
108        assert!(s.merge(s).is_synthetic());
109    }
110
111    #[test]
112    fn line_col_counts_newlines() {
113        let src = "abc\nde\nfghi";
114        assert_eq!(Span::line_col(src, 0), (1, 1));
115        assert_eq!(Span::line_col(src, 2), (1, 3));
116        assert_eq!(Span::line_col(src, 4), (2, 1));
117        assert_eq!(Span::line_col(src, 7), (3, 1));
118        assert_eq!(Span::line_col(src, 10), (3, 4));
119    }
120
121    #[test]
122    fn display_synthetic() {
123        assert_eq!(Span::synthetic().to_string(), "<synthetic>");
124    }
125
126    #[test]
127    fn display_real() {
128        assert_eq!(Span::new(4, 9).to_string(), "4..9");
129    }
130}