Skip to main content

rl_utils/
line_index.rs

1use std::sync::Arc;
2
3/// A compact, serializable mapping from byte offsets to 1-indexed
4/// `(line, column)` pairs.
5///
6/// Computed once from source text at compile time. Unlike [`crate::source::SourceFile`],
7/// this does not retain the source text itself - only the byte offset
8/// where each line starts - so it's cheap enough to embed in compiled
9/// `.rlc` bytecode (which intentionally doesn't ship the original
10/// source). This lets runtime errors raised from `.rlc` bytecode still
11/// report a precise `file:line:col` location instead of a bare message,
12/// without paying the cost (or leaking the source) of a full ariadne
13/// snippet.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct LineIndex {
16    /// Displayed in error headers (e.g. `"main.rl"`).
17    source_name: Arc<str>,
18    /// Byte offset of the start of each line; `line_starts[0] == 0`.
19    line_starts: Vec<u32>,
20}
21
22impl LineIndex {
23    /// Builds a [`LineIndex`] by scanning `text` for line breaks.
24    pub fn new(source_name: impl Into<Arc<str>>, text: &str) -> Self {
25        let mut line_starts = vec![0u32];
26        line_starts.extend(
27            text.bytes()
28                .enumerate()
29                .filter(|&(_, b)| b == b'\n')
30                .map(|(i, _)| (i + 1) as u32),
31        );
32        Self {
33            source_name: source_name.into(),
34            line_starts,
35        }
36    }
37
38    /// Reconstructs a [`LineIndex`] from its raw parts (used when
39    /// deserializing from a `.rlc` file).
40    pub fn from_raw(source_name: impl Into<Arc<str>>, line_starts: Vec<u32>) -> Self {
41        Self {
42            source_name: source_name.into(),
43            line_starts,
44        }
45    }
46
47    pub fn source_name(&self) -> &Arc<str> {
48        &self.source_name
49    }
50
51    pub fn line_starts(&self) -> &[u32] {
52        &self.line_starts
53    }
54
55    /// 1-indexed `(line, column)` for a byte offset. Clamps out-of-range
56    /// offsets to the last known line rather than panicking, since a
57    /// slightly-stale span shouldn't crash the error reporter.
58    pub fn line_col(&self, offset: usize) -> (usize, usize) {
59        let offset = offset as u32;
60        let line = match self.line_starts.binary_search(&offset) {
61            Ok(i) => i,
62            Err(i) => i.saturating_sub(1),
63        };
64        let line_start = self.line_starts.get(line).copied().unwrap_or(0);
65        (line + 1, (offset.saturating_sub(line_start)) as usize + 1)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::LineIndex;
72
73    #[test]
74    fn line_col_basic() {
75        let idx = LineIndex::new("t.rl", "let a = 1;\nlet b = 2;\nprint(a);");
76        assert_eq!(idx.line_col(0), (1, 1));
77        assert_eq!(idx.line_col(11), (2, 1));
78        assert_eq!(idx.line_col(15), (2, 5));
79        assert_eq!(idx.line_col(22), (3, 1));
80    }
81}