Skip to main content

wave_compiler/diagnostics/
source_map.rs

1// Copyright 2026 Ojima Abraham
2// SPDX-License-Identifier: Apache-2.0
3
4//! Source location tracking for error messages.
5//!
6//! Maps byte offsets in compiled code back to source file positions
7//! for user-facing error reporting.
8
9use super::error::SourceLoc;
10
11/// Maps source code positions to line/column numbers.
12pub struct SourceMap {
13    source: String,
14    line_starts: Vec<usize>,
15}
16
17impl SourceMap {
18    /// Create a new source map from source code.
19    #[must_use]
20    pub fn new(source: String) -> Self {
21        let mut line_starts = vec![0];
22        for (i, ch) in source.char_indices() {
23            if ch == '\n' {
24                line_starts.push(i + 1);
25            }
26        }
27        Self {
28            source,
29            line_starts,
30        }
31    }
32
33    /// Convert a byte offset to a source location.
34    #[must_use]
35    pub fn offset_to_loc(&self, offset: usize) -> SourceLoc {
36        let line = self
37            .line_starts
38            .partition_point(|&start| start <= offset)
39            .saturating_sub(1);
40        let col = offset - self.line_starts[line];
41        SourceLoc {
42            #[allow(clippy::cast_possible_truncation)]
43            line: (line + 1) as u32,
44            #[allow(clippy::cast_possible_truncation)]
45            col: (col + 1) as u32,
46        }
47    }
48
49    /// Get a line of source code by line number (1-based).
50    #[must_use]
51    pub fn get_line(&self, line: u32) -> Option<&str> {
52        let idx = (line as usize).checked_sub(1)?;
53        let start = *self.line_starts.get(idx)?;
54        let end = self
55            .line_starts
56            .get(idx + 1)
57            .map_or(self.source.len(), |&s| s.saturating_sub(1));
58        Some(&self.source[start..end])
59    }
60
61    /// Returns the total number of lines.
62    #[must_use]
63    pub fn line_count(&self) -> usize {
64        self.line_starts.len()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_offset_to_loc() {
74        let src = "line1\nline2\nline3".to_string();
75        let map = SourceMap::new(src);
76        assert_eq!(map.offset_to_loc(0), SourceLoc { line: 1, col: 1 });
77        assert_eq!(map.offset_to_loc(6), SourceLoc { line: 2, col: 1 });
78        assert_eq!(map.offset_to_loc(8), SourceLoc { line: 2, col: 3 });
79    }
80
81    #[test]
82    fn test_get_line() {
83        let src = "first\nsecond\nthird".to_string();
84        let map = SourceMap::new(src);
85        assert_eq!(map.get_line(1), Some("first"));
86        assert_eq!(map.get_line(2), Some("second"));
87        assert_eq!(map.get_line(3), Some("third"));
88        assert_eq!(map.get_line(4), None);
89    }
90
91    #[test]
92    fn test_line_count() {
93        let src = "a\nb\nc\n".to_string();
94        let map = SourceMap::new(src);
95        assert_eq!(map.line_count(), 4);
96    }
97}