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(
244                src.match_indices('\n')
245                    .map(|(i, _)| BytePos::new(i as u32 + 1)),
246            )
247            .collect();
248
249        Self {
250            id,
251            name,
252            src,
253            line_starts,
254        }
255    }
256
257    /// Get the line/column for a byte position.
258    #[must_use]
259    pub fn lookup_line_col(&self, pos: BytePos) -> LineCol {
260        let line_idx = self
261            .line_starts
262            .partition_point(|&start| start.0 <= pos.0)
263            .saturating_sub(1);
264
265        let line_start = self.line_starts[line_idx];
266        let col = pos.0 - line_start.0 + 1;
267
268        LineCol {
269            line: line_idx as u32 + 1,
270            col,
271        }
272    }
273
274    /// Get the 0-indexed line number for a byte position.
275    #[must_use]
276    pub fn lookup_line(&self, pos: BytePos) -> usize {
277        self.line_starts
278            .partition_point(|&start| start.0 <= pos.0)
279            .saturating_sub(1)
280    }
281
282    /// Get the source text for a span.
283    #[must_use]
284    pub fn source_text(&self, span: Span) -> &str {
285        &self.src[span.lo.as_usize()..span.hi.as_usize()]
286    }
287
288    /// Get the number of lines in the file.
289    #[must_use]
290    pub fn num_lines(&self) -> usize {
291        self.line_starts.len()
292    }
293
294    /// Get the content of a specific line (0-indexed).
295    #[must_use]
296    pub fn line_content(&self, line_idx: usize) -> Option<&str> {
297        if line_idx >= self.line_starts.len() {
298            return None;
299        }
300
301        let start = self.line_starts[line_idx].as_usize();
302        let end = if line_idx + 1 < self.line_starts.len() {
303            // Next line start minus 1 to exclude the newline
304            self.line_starts[line_idx + 1].as_usize().saturating_sub(1)
305        } else {
306            self.src.len()
307        };
308
309        Some(&self.src[start..end])
310    }
311
312    /// Get the byte offset of the start of a line (0-indexed).
313    #[must_use]
314    pub fn line_start(&self, line_idx: usize) -> Option<BytePos> {
315        self.line_starts.get(line_idx).copied()
316    }
317
318    /// Get span information for rendering: start line, start col, end line, end col.
319    ///
320    /// Note: Spans are half-open `[lo, hi)`, so the end position is computed
321    /// from the last included byte (`hi - 1`) for non-empty spans.
322    #[must_use]
323    pub fn span_lines(&self, span: Span) -> SpanLines {
324        let start = self.lookup_line_col(span.lo);
325        // For the end, use the last included byte (hi - 1) for non-empty spans
326        let end = if span.hi.0 > span.lo.0 {
327            self.lookup_line_col(BytePos(span.hi.0 - 1))
328        } else {
329            start
330        };
331        SpanLines {
332            start_line: start.line as usize,
333            start_col: start.col as usize,
334            end_line: end.line as usize,
335            // For end column, add 1 since we want the position after the last char
336            end_col: end.col as usize + 1,
337        }
338    }
339}
340
341/// Information about which lines a span covers.
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub struct SpanLines {
344    /// 1-indexed start line.
345    pub start_line: usize,
346    /// 1-indexed start column.
347    pub start_col: usize,
348    /// 1-indexed end line.
349    pub end_line: usize,
350    /// 1-indexed end column.
351    pub end_col: usize,
352}
353
354impl SpanLines {
355    /// Check if this span covers multiple lines.
356    #[must_use]
357    pub fn is_multiline(&self) -> bool {
358        self.start_line != self.end_line
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_span_operations() {
368        let span1 = Span::from_raw(10, 20);
369        let span2 = Span::from_raw(15, 30);
370
371        assert_eq!(span1.len(), 10);
372        assert_eq!(span1.merge(span2), Span::from_raw(10, 30));
373        assert!(span1.contains(BytePos::new(15)));
374        assert!(!span1.contains(BytePos::new(25)));
375    }
376
377    #[test]
378    fn test_source_file_line_lookup() {
379        let src = "line 1\nline 2\nline 3";
380        let file = SourceFile::new(FileId::new(0), "test.hs".to_string(), src.to_string());
381
382        assert_eq!(file.lookup_line_col(BytePos::new(0)), LineCol::new(1, 1));
383        assert_eq!(file.lookup_line_col(BytePos::new(7)), LineCol::new(2, 1));
384        assert_eq!(file.lookup_line_col(BytePos::new(10)), LineCol::new(2, 4));
385    }
386
387    #[test]
388    fn test_line_content() {
389        let src = "first line\nsecond line\nthird line";
390        let file = SourceFile::new(FileId::new(0), "test.hs".to_string(), src.to_string());
391
392        assert_eq!(file.line_content(0), Some("first line"));
393        assert_eq!(file.line_content(1), Some("second line"));
394        assert_eq!(file.line_content(2), Some("third line"));
395        assert_eq!(file.line_content(3), None);
396    }
397
398    #[test]
399    fn test_span_lines() {
400        let src = "line 1\nline 2\nline 3";
401        let file = SourceFile::new(FileId::new(0), "test.hs".to_string(), src.to_string());
402
403        // Single line span
404        let span = Span::from_raw(0, 6);
405        let lines = file.span_lines(span);
406        assert_eq!(lines.start_line, 1);
407        assert_eq!(lines.end_line, 1);
408        assert!(!lines.is_multiline());
409
410        // Multi-line span
411        let span = Span::from_raw(0, 14);
412        let lines = file.span_lines(span);
413        assert_eq!(lines.start_line, 1);
414        assert_eq!(lines.end_line, 2);
415        assert!(lines.is_multiline());
416    }
417}