Skip to main content

pepl_types/
span.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Source location span.
5///
6/// All line/column values are 1-based for human-readable error messages.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct Span {
9    #[serde(rename = "line")]
10    pub start_line: u32,
11    #[serde(rename = "column")]
12    pub start_col: u32,
13    pub end_line: u32,
14    #[serde(rename = "end_column")]
15    pub end_col: u32,
16}
17
18impl Span {
19    /// Create a new span.
20    pub fn new(start_line: u32, start_col: u32, end_line: u32, end_col: u32) -> Self {
21        Self {
22            start_line,
23            start_col,
24            end_line,
25            end_col,
26        }
27    }
28
29    /// Create a zero-width span at a single position.
30    pub fn point(line: u32, col: u32) -> Self {
31        Self::new(line, col, line, col)
32    }
33
34    /// Merge two spans into one that covers both.
35    pub fn merge(self, other: Span) -> Span {
36        let start_line = self.start_line.min(other.start_line);
37        let start_col = if self.start_line < other.start_line {
38            self.start_col
39        } else if other.start_line < self.start_line {
40            other.start_col
41        } else {
42            self.start_col.min(other.start_col)
43        };
44
45        let end_line = self.end_line.max(other.end_line);
46        let end_col = if self.end_line > other.end_line {
47            self.end_col
48        } else if other.end_line > self.end_line {
49            other.end_col
50        } else {
51            self.end_col.max(other.end_col)
52        };
53
54        Span::new(start_line, start_col, end_line, end_col)
55    }
56}
57
58impl fmt::Display for Span {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{}:{}", self.start_line, self.start_col)
61    }
62}
63
64/// Holds the source text for error reporting.
65#[derive(Debug, Clone)]
66pub struct SourceFile {
67    pub name: String,
68    pub source: String,
69    /// Cached line start byte offsets for fast line lookup.
70    line_starts: Vec<usize>,
71}
72
73impl SourceFile {
74    /// Create a new source file.
75    pub fn new(name: impl Into<String>, source: impl Into<String>) -> Self {
76        let source = source.into();
77        let line_starts = std::iter::once(0)
78            .chain(source.match_indices('\n').map(|(i, _)| i + 1))
79            .collect();
80        Self {
81            name: name.into(),
82            source,
83            line_starts,
84        }
85    }
86
87    /// Extract a source line by 1-based line number.
88    ///
89    /// Returns `None` if the line number is out of range.
90    pub fn line(&self, line_number: u32) -> Option<&str> {
91        let idx = line_number.checked_sub(1)? as usize;
92        if idx >= self.line_starts.len() {
93            return None;
94        }
95        let start = self.line_starts[idx];
96        let end = self
97            .line_starts
98            .get(idx + 1)
99            .map(|&s| s.saturating_sub(1)) // strip the \n
100            .unwrap_or(self.source.len());
101        let line = &self.source[start..end];
102        // Also strip trailing \r for CRLF
103        Some(line.trim_end_matches('\r'))
104    }
105
106    /// Get the total number of lines.
107    pub fn line_count(&self) -> usize {
108        self.line_starts.len()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_span_point() {
118        let s = Span::point(1, 5);
119        assert_eq!(s.start_line, 1);
120        assert_eq!(s.start_col, 5);
121        assert_eq!(s.end_line, 1);
122        assert_eq!(s.end_col, 5);
123    }
124
125    #[test]
126    fn test_span_merge() {
127        let a = Span::new(1, 5, 1, 10);
128        let b = Span::new(2, 3, 2, 8);
129        let merged = a.merge(b);
130        assert_eq!(merged.start_line, 1);
131        assert_eq!(merged.start_col, 5);
132        assert_eq!(merged.end_line, 2);
133        assert_eq!(merged.end_col, 8);
134    }
135
136    #[test]
137    fn test_span_merge_same_line() {
138        let a = Span::new(1, 5, 1, 10);
139        let b = Span::new(1, 3, 1, 8);
140        let merged = a.merge(b);
141        assert_eq!(merged.start_col, 3);
142        assert_eq!(merged.end_col, 10);
143    }
144
145    #[test]
146    fn test_span_display() {
147        let s = Span::new(3, 7, 3, 15);
148        assert_eq!(format!("{s}"), "3:7");
149    }
150
151    #[test]
152    fn test_source_file_line_extraction() {
153        let src = SourceFile::new("test.pepl", "line one\nline two\nline three");
154        assert_eq!(src.line(1), Some("line one"));
155        assert_eq!(src.line(2), Some("line two"));
156        assert_eq!(src.line(3), Some("line three"));
157        assert_eq!(src.line(0), None);
158        assert_eq!(src.line(4), None);
159    }
160
161    #[test]
162    fn test_source_file_crlf() {
163        let src = SourceFile::new("test.pepl", "line one\r\nline two\r\n");
164        assert_eq!(src.line(1), Some("line one"));
165        assert_eq!(src.line(2), Some("line two"));
166    }
167
168    #[test]
169    fn test_source_file_line_count() {
170        let src = SourceFile::new("test.pepl", "a\nb\nc");
171        assert_eq!(src.line_count(), 3);
172    }
173
174    #[test]
175    fn test_source_file_empty() {
176        let src = SourceFile::new("test.pepl", "");
177        assert_eq!(src.line_count(), 1);
178        assert_eq!(src.line(1), Some(""));
179    }
180
181    #[test]
182    fn test_span_determinism_100_iterations() {
183        let input_a = Span::new(1, 5, 1, 10);
184        let input_b = Span::new(2, 3, 2, 8);
185        let first = input_a.merge(input_b);
186        for i in 0..100 {
187            let result = input_a.merge(input_b);
188            assert_eq!(first, result, "Determinism failure at iteration {i}");
189        }
190    }
191
192    #[test]
193    fn test_source_file_determinism_100_iterations() {
194        let source_text = "space Counter {\n  state {\n    count: number = 0\n  }\n}";
195        let first_file = SourceFile::new("test.pepl", source_text);
196        let first_line2 = first_file.line(2).map(String::from);
197        for i in 0..100 {
198            let file = SourceFile::new("test.pepl", source_text);
199            let line2 = file.line(2).map(String::from);
200            assert_eq!(first_line2, line2, "Determinism failure at iteration {i}");
201        }
202    }
203}