Skip to main content

bhc_span/
lib.rs

1//! Source location tracking and span management for BHC.
2//!
3//! This crate provides types for tracking source locations throughout
4//! the compilation pipeline, enabling accurate error reporting and
5//! source mapping.
6
7#![warn(missing_docs)]
8
9use serde::{Deserialize, Serialize};
10
11/// A byte offset into a source file.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[repr(transparent)]
14pub struct BytePos(pub u32);
15
16impl BytePos {
17    /// The zero position.
18    pub const ZERO: Self = Self(0);
19
20    /// Create a new byte position.
21    #[must_use]
22    pub const fn new(pos: u32) -> Self {
23        Self(pos)
24    }
25
26    /// Get the raw byte offset.
27    #[must_use]
28    pub const fn as_u32(self) -> u32 {
29        self.0
30    }
31
32    /// Get the raw byte offset as usize.
33    #[must_use]
34    pub const fn as_usize(self) -> usize {
35        self.0 as usize
36    }
37}
38
39impl std::ops::Add<u32> for BytePos {
40    type Output = Self;
41
42    fn add(self, rhs: u32) -> Self::Output {
43        Self(self.0 + rhs)
44    }
45}
46
47impl std::ops::Sub for BytePos {
48    type Output = u32;
49
50    fn sub(self, rhs: Self) -> Self::Output {
51        self.0 - rhs.0
52    }
53}
54
55/// A span of source code, represented as a half-open byte range [lo, hi).
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub struct Span {
58    /// The start of the span (inclusive).
59    pub lo: BytePos,
60    /// The end of the span (exclusive).
61    pub hi: BytePos,
62}
63
64impl Span {
65    /// A dummy span for generated code or when location is irrelevant.
66    pub const DUMMY: Self = Self {
67        lo: BytePos::ZERO,
68        hi: BytePos::ZERO,
69    };
70
71    /// Create a new span from byte positions.
72    #[must_use]
73    pub const fn new(lo: BytePos, hi: BytePos) -> Self {
74        Self { lo, hi }
75    }
76
77    /// Create a span from raw byte offsets.
78    #[must_use]
79    pub const fn from_raw(lo: u32, hi: u32) -> Self {
80        Self {
81            lo: BytePos(lo),
82            hi: BytePos(hi),
83        }
84    }
85
86    /// Check if this is a dummy span.
87    #[must_use]
88    pub const fn is_dummy(self) -> bool {
89        self.lo.0 == 0 && self.hi.0 == 0
90    }
91
92    /// Get the length of the span in bytes.
93    #[must_use]
94    pub const fn len(self) -> u32 {
95        self.hi.0 - self.lo.0
96    }
97
98    /// Check if the span is empty.
99    #[must_use]
100    pub const fn is_empty(self) -> bool {
101        self.lo.0 == self.hi.0
102    }
103
104    /// Merge two spans into one that covers both.
105    #[must_use]
106    pub fn merge(self, other: Self) -> Self {
107        Self {
108            lo: BytePos(self.lo.0.min(other.lo.0)),
109            hi: BytePos(self.hi.0.max(other.hi.0)),
110        }
111    }
112
113    /// Create a span that covers from the start of self to the end of other.
114    #[must_use]
115    pub const fn to(self, other: Self) -> Self {
116        Self {
117            lo: self.lo,
118            hi: other.hi,
119        }
120    }
121
122    /// Shrink the span to a single point at the start.
123    #[must_use]
124    pub const fn shrink_to_lo(self) -> Self {
125        Self {
126            lo: self.lo,
127            hi: self.lo,
128        }
129    }
130
131    /// Shrink the span to a single point at the end.
132    #[must_use]
133    pub const fn shrink_to_hi(self) -> Self {
134        Self {
135            lo: self.hi,
136            hi: self.hi,
137        }
138    }
139
140    /// Check if this span contains the given byte position.
141    #[must_use]
142    pub const fn contains(self, pos: BytePos) -> bool {
143        self.lo.0 <= pos.0 && pos.0 < self.hi.0
144    }
145}
146
147impl Default for Span {
148    fn default() -> Self {
149        Self::DUMMY
150    }
151}
152
153/// A value with an associated span.
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
155pub struct Spanned<T> {
156    /// The value.
157    pub node: T,
158    /// The span of the value in source code.
159    pub span: Span,
160}
161
162impl<T> Spanned<T> {
163    /// Create a new spanned value.
164    #[must_use]
165    pub const fn new(node: T, span: Span) -> Self {
166        Self { node, span }
167    }
168
169    /// Map the inner value while preserving the span.
170    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Spanned<U> {
171        Spanned {
172            node: f(self.node),
173            span: self.span,
174        }
175    }
176}
177
178/// A unique identifier for a source file.
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
180#[repr(transparent)]
181pub struct FileId(pub u32);
182
183impl FileId {
184    /// Create a new file ID.
185    #[must_use]
186    pub const fn new(id: u32) -> Self {
187        Self(id)
188    }
189}
190
191/// A span with an associated file ID for cross-file spans.
192#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
193pub struct FullSpan {
194    /// The file this span belongs to.
195    pub file: FileId,
196    /// The span within the file.
197    pub span: Span,
198}
199
200impl FullSpan {
201    /// Create a new full span.
202    #[must_use]
203    pub const fn new(file: FileId, span: Span) -> Self {
204        Self { file, span }
205    }
206}
207
208/// Line and column information for a source location.
209#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
210pub struct LineCol {
211    /// 1-indexed line number.
212    pub line: u32,
213    /// 1-indexed column number (in UTF-8 code units).
214    pub col: u32,
215}
216
217impl LineCol {
218    /// Create a new line/column pair.
219    #[must_use]
220    pub const fn new(line: u32, col: u32) -> Self {
221        Self { line, col }
222    }
223}
224
225/// Information about a source file.
226#[derive(Clone, Debug)]
227pub struct SourceFile {
228    /// The file ID.
229    pub id: FileId,
230    /// The file name or path.
231    pub name: String,
232    /// The source code content.
233    pub src: String,
234    /// Byte offsets of line starts.
235    line_starts: Vec<BytePos>,
236}
237
238impl SourceFile {
239    /// Create a new source file.
240    #[must_use]
241    pub fn new(id: FileId, name: String, src: String) -> Self {
242        let line_starts = std::iter::once(BytePos::ZERO)
243            .chain(src.match_indices('\n').map(|(i, _)| BytePos::new(i as u32 + 1)))
244            .collect();
245
246        Self {
247            id,
248            name,
249            src,
250            line_starts,
251        }
252    }
253
254    /// Get the line/column for a byte position.
255    #[must_use]
256    pub fn lookup_line_col(&self, pos: BytePos) -> LineCol {
257        let line_idx = self
258            .line_starts
259            .partition_point(|&start| start.0 <= pos.0)
260            .saturating_sub(1);
261
262        let line_start = self.line_starts[line_idx];
263        let col = pos.0 - line_start.0 + 1;
264
265        LineCol {
266            line: line_idx as u32 + 1,
267            col,
268        }
269    }
270
271    /// Get the 0-indexed line number for a byte position.
272    #[must_use]
273    pub fn lookup_line(&self, pos: BytePos) -> usize {
274        self.line_starts
275            .partition_point(|&start| start.0 <= pos.0)
276            .saturating_sub(1)
277    }
278
279    /// Get the source text for a span.
280    #[must_use]
281    pub fn source_text(&self, span: Span) -> &str {
282        &self.src[span.lo.as_usize()..span.hi.as_usize()]
283    }
284
285    /// Get the number of lines in the file.
286    #[must_use]
287    pub fn num_lines(&self) -> usize {
288        self.line_starts.len()
289    }
290
291    /// Get the content of a specific line (0-indexed).
292    #[must_use]
293    pub fn line_content(&self, line_idx: usize) -> Option<&str> {
294        if line_idx >= self.line_starts.len() {
295            return None;
296        }
297
298        let start = self.line_starts[line_idx].as_usize();
299        let end = if line_idx + 1 < self.line_starts.len() {
300            // Next line start minus 1 to exclude the newline
301            self.line_starts[line_idx + 1].as_usize().saturating_sub(1)
302        } else {
303            self.src.len()
304        };
305
306        Some(&self.src[start..end])
307    }
308
309    /// Get the byte offset of the start of a line (0-indexed).
310    #[must_use]
311    pub fn line_start(&self, line_idx: usize) -> Option<BytePos> {
312        self.line_starts.get(line_idx).copied()
313    }
314
315    /// Get span information for rendering: start line, start col, end line, end col.
316    ///
317    /// Note: Spans are half-open `[lo, hi)`, so the end position is computed
318    /// from the last included byte (`hi - 1`) for non-empty spans.
319    #[must_use]
320    pub fn span_lines(&self, span: Span) -> SpanLines {
321        let start = self.lookup_line_col(span.lo);
322        // For the end, use the last included byte (hi - 1) for non-empty spans
323        let end = if span.hi.0 > span.lo.0 {
324            self.lookup_line_col(BytePos(span.hi.0 - 1))
325        } else {
326            start
327        };
328        SpanLines {
329            start_line: start.line as usize,
330            start_col: start.col as usize,
331            end_line: end.line as usize,
332            // For end column, add 1 since we want the position after the last char
333            end_col: end.col as usize + 1,
334        }
335    }
336}
337
338/// Information about which lines a span covers.
339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
340pub struct SpanLines {
341    /// 1-indexed start line.
342    pub start_line: usize,
343    /// 1-indexed start column.
344    pub start_col: usize,
345    /// 1-indexed end line.
346    pub end_line: usize,
347    /// 1-indexed end column.
348    pub end_col: usize,
349}
350
351impl SpanLines {
352    /// Check if this span covers multiple lines.
353    #[must_use]
354    pub fn is_multiline(&self) -> bool {
355        self.start_line != self.end_line
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_span_operations() {
365        let span1 = Span::from_raw(10, 20);
366        let span2 = Span::from_raw(15, 30);
367
368        assert_eq!(span1.len(), 10);
369        assert_eq!(span1.merge(span2), Span::from_raw(10, 30));
370        assert!(span1.contains(BytePos::new(15)));
371        assert!(!span1.contains(BytePos::new(25)));
372    }
373
374    #[test]
375    fn test_source_file_line_lookup() {
376        let src = "line 1\nline 2\nline 3";
377        let file = SourceFile::new(FileId::new(0), "test.hs".to_string(), src.to_string());
378
379        assert_eq!(file.lookup_line_col(BytePos::new(0)), LineCol::new(1, 1));
380        assert_eq!(file.lookup_line_col(BytePos::new(7)), LineCol::new(2, 1));
381        assert_eq!(file.lookup_line_col(BytePos::new(10)), LineCol::new(2, 4));
382    }
383
384    #[test]
385    fn test_line_content() {
386        let src = "first line\nsecond line\nthird line";
387        let file = SourceFile::new(FileId::new(0), "test.hs".to_string(), src.to_string());
388
389        assert_eq!(file.line_content(0), Some("first line"));
390        assert_eq!(file.line_content(1), Some("second line"));
391        assert_eq!(file.line_content(2), Some("third line"));
392        assert_eq!(file.line_content(3), None);
393    }
394
395    #[test]
396    fn test_span_lines() {
397        let src = "line 1\nline 2\nline 3";
398        let file = SourceFile::new(FileId::new(0), "test.hs".to_string(), src.to_string());
399
400        // Single line span
401        let span = Span::from_raw(0, 6);
402        let lines = file.span_lines(span);
403        assert_eq!(lines.start_line, 1);
404        assert_eq!(lines.end_line, 1);
405        assert!(!lines.is_multiline());
406
407        // Multi-line span
408        let span = Span::from_raw(0, 14);
409        let lines = file.span_lines(span);
410        assert_eq!(lines.start_line, 1);
411        assert_eq!(lines.end_line, 2);
412        assert!(lines.is_multiline());
413    }
414}