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}