1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[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 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 pub fn point(line: u32, col: u32) -> Self {
31 Self::new(line, col, line, col)
32 }
33
34 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#[derive(Debug, Clone)]
66pub struct SourceFile {
67 pub name: String,
68 pub source: String,
69 line_starts: Vec<usize>,
71}
72
73impl SourceFile {
74 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 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)) .unwrap_or(self.source.len());
101 let line = &self.source[start..end];
102 Some(line.trim_end_matches('\r'))
104 }
105
106 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}