Skip to main content

caixa_ast/
span.rs

1//! Byte-offset spans — minimal and cheap. Line/column are computed on demand.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// A half-open byte range `[start, end)` into some source string.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
9pub struct Span {
10    pub start: u32,
11    pub end: u32,
12}
13
14impl Span {
15    #[must_use]
16    pub const fn new(start: u32, end: u32) -> Self {
17        Self { start, end }
18    }
19
20    #[must_use]
21    pub const fn point(offset: u32) -> Self {
22        Self {
23            start: offset,
24            end: offset,
25        }
26    }
27
28    #[must_use]
29    pub fn len(self) -> u32 {
30        self.end.saturating_sub(self.start)
31    }
32
33    #[must_use]
34    pub fn is_empty(self) -> bool {
35        self.len() == 0
36    }
37
38    /// The smallest span covering both. Useful for building a list node's
39    /// span from its children.
40    #[must_use]
41    pub fn union(self, other: Span) -> Span {
42        Span {
43            start: self.start.min(other.start),
44            end: self.end.max(other.end),
45        }
46    }
47
48    #[must_use]
49    pub fn slice<'a>(self, src: &'a str) -> &'a str {
50        let start = self.start as usize;
51        let end = self.end as usize;
52        if start >= src.len() {
53            ""
54        } else {
55            let end = end.min(src.len());
56            &src[start..end]
57        }
58    }
59
60    #[must_use]
61    pub fn contains(self, offset: u32) -> bool {
62        offset >= self.start && offset < self.end
63    }
64}
65
66impl fmt::Display for Span {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "{}..{}", self.start, self.end)
69    }
70}
71
72/// 1-indexed line/column pair — what humans see in editors.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub struct Position {
75    pub line: u32,
76    pub column: u32,
77}
78
79impl fmt::Display for Position {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(f, "{}:{}", self.line, self.column)
82    }
83}
84
85/// Compute (line, column) for a byte offset. Line and column are 1-indexed.
86/// O(offset); fine for diagnostics, not for hot paths.
87#[must_use]
88pub fn line_column(src: &str, offset: u32) -> Position {
89    let mut line: u32 = 1;
90    let mut col: u32 = 1;
91    let offset = offset as usize;
92    for (i, ch) in src.char_indices() {
93        if i >= offset {
94            break;
95        }
96        if ch == '\n' {
97            line += 1;
98            col = 1;
99        } else {
100            col += 1;
101        }
102    }
103    Position { line, column: col }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn point_span_is_empty() {
112        let s = Span::point(5);
113        assert!(s.is_empty());
114        assert_eq!(s.len(), 0);
115    }
116
117    #[test]
118    fn union_widens() {
119        let a = Span::new(2, 5);
120        let b = Span::new(4, 9);
121        let u = a.union(b);
122        assert_eq!(u.start, 2);
123        assert_eq!(u.end, 9);
124    }
125
126    #[test]
127    fn slice_extracts_substring() {
128        let src = "hello world";
129        assert_eq!(Span::new(6, 11).slice(src), "world");
130    }
131
132    #[test]
133    fn line_column_handles_newlines() {
134        let src = "abc\ndef\nghi";
135        assert_eq!(line_column(src, 0), Position { line: 1, column: 1 });
136        assert_eq!(line_column(src, 4), Position { line: 2, column: 1 });
137        assert_eq!(line_column(src, 9), Position { line: 3, column: 2 });
138    }
139}