Skip to main content

vm/compiler/
source_map.rs

1use std::ops::Range;
2
3pub type SourceId = u32;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct Span {
7    pub source_id: SourceId,
8    pub lo: usize,
9    pub hi: usize,
10}
11
12impl Span {
13    pub fn new(source_id: SourceId, lo: usize, hi: usize) -> Self {
14        if lo <= hi {
15            Self { source_id, lo, hi }
16        } else {
17            Self {
18                source_id,
19                lo: hi,
20                hi: lo,
21            }
22        }
23    }
24
25    pub fn len(self) -> usize {
26        self.hi.saturating_sub(self.lo)
27    }
28
29    pub fn is_empty(self) -> bool {
30        self.lo == self.hi
31    }
32}
33
34#[derive(Clone, Debug)]
35pub struct SourceFile {
36    pub id: SourceId,
37    pub name: String,
38    pub text: String,
39    line_starts: Vec<usize>,
40}
41
42impl SourceFile {
43    fn new(id: SourceId, name: String, text: String) -> Self {
44        let line_starts = compute_line_starts(&text);
45        Self {
46            id,
47            name,
48            text,
49            line_starts,
50        }
51    }
52
53    pub fn line_count(&self) -> usize {
54        self.line_starts.len()
55    }
56
57    pub fn line_col_for_offset(&self, offset: usize) -> Option<(usize, usize)> {
58        if offset > self.text.len() {
59            return None;
60        }
61        let line_idx = line_index_for_offset(&self.line_starts, offset)?;
62        let line_start = self.line_starts[line_idx];
63        let col = self.text[line_start..offset].chars().count() + 1;
64        Some((line_idx + 1, col))
65    }
66
67    pub fn line_span(&self, line: usize) -> Option<Range<usize>> {
68        if line == 0 || line > self.line_starts.len() {
69            return None;
70        }
71        let idx = line - 1;
72        let start = self.line_starts[idx];
73        let end = if idx + 1 < self.line_starts.len() {
74            self.line_starts[idx + 1]
75        } else {
76            self.text.len()
77        };
78        let line_text = &self.text[start..end];
79        let trimmed_end = line_text.trim_end_matches(['\n', '\r']).len();
80        Some(start..start + trimmed_end)
81    }
82
83    pub fn line_text(&self, line: usize) -> Option<&str> {
84        let range = self.line_span(line)?;
85        self.text.get(range)
86    }
87
88    pub fn line_col_to_offset(&self, line: usize, col: usize) -> Option<usize> {
89        if line == 0 || line > self.line_starts.len() || col == 0 {
90            return None;
91        }
92        let line_range = self.line_span(line)?;
93        let mut byte = line_range.start;
94        let mut current_col = 1usize;
95        while byte < line_range.end && current_col < col {
96            let ch = self.text[byte..].chars().next()?;
97            byte += ch.len_utf8();
98            current_col += 1;
99        }
100        Some(byte)
101    }
102}
103
104#[derive(Clone, Debug, Default)]
105pub struct SourceMap {
106    files: Vec<SourceFile>,
107}
108
109impl SourceMap {
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    pub fn add_source(&mut self, name: impl Into<String>, text: impl Into<String>) -> SourceId {
115        let id = self.files.len() as SourceId;
116        self.files
117            .push(SourceFile::new(id, name.into(), text.into()));
118        id
119    }
120
121    pub fn file(&self, id: SourceId) -> Option<&SourceFile> {
122        self.files.get(id as usize)
123    }
124
125    pub fn source_id_by_name(&self, name: &str) -> Option<SourceId> {
126        self.files
127            .iter()
128            .find(|file| file.name == name)
129            .map(|file| file.id)
130    }
131
132    pub fn source(&self, id: SourceId) -> Option<&str> {
133        self.file(id).map(|file| file.text.as_str())
134    }
135
136    pub fn line_span(&self, id: SourceId, line: usize) -> Option<Span> {
137        let file = self.file(id)?;
138        let range = file.line_span(line)?;
139        Some(Span::new(id, range.start, range.end))
140    }
141
142    pub fn line_col_for_offset(&self, id: SourceId, offset: usize) -> Option<(usize, usize)> {
143        self.file(id)?.line_col_for_offset(offset)
144    }
145
146    pub fn line_col_to_offset(&self, id: SourceId, line: usize, col: usize) -> Option<usize> {
147        self.file(id)?.line_col_to_offset(line, col)
148    }
149
150    pub fn span_text(&self, span: Span) -> Option<&str> {
151        let file = self.file(span.source_id)?;
152        file.text.get(span.lo..span.hi)
153    }
154}
155
156#[derive(Clone, Debug, PartialEq, Eq)]
157pub struct LineSpanMapping {
158    pub lowered_to_original_line: Vec<usize>,
159}
160
161impl LineSpanMapping {
162    pub fn identity(source: &str) -> Self {
163        let lines = source.lines().count().max(1);
164        Self {
165            lowered_to_original_line: (1..=lines).collect(),
166        }
167    }
168
169    pub fn map_span(
170        &self,
171        source_map: &SourceMap,
172        lowered_source_id: SourceId,
173        original_source_id: SourceId,
174        lowered_span: Span,
175    ) -> Option<Span> {
176        if lowered_span.source_id != lowered_source_id {
177            return None;
178        }
179        let (lowered_line, lowered_col) =
180            source_map.line_col_for_offset(lowered_source_id, lowered_span.lo)?;
181        let original_line = *self
182            .lowered_to_original_line
183            .get(lowered_line.saturating_sub(1))
184            .unwrap_or(&lowered_line);
185        let original_file = source_map.file(original_source_id)?;
186        let line_range = original_file.line_span(original_line)?;
187        let lo = original_file
188            .line_col_to_offset(original_line, lowered_col)
189            .unwrap_or(line_range.start);
190        let hi = if lowered_span.is_empty() {
191            lo
192        } else {
193            line_range.end.min(lo.saturating_add(lowered_span.len()))
194        };
195        Some(Span::new(original_source_id, lo, hi))
196    }
197}
198
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct LoweredSource {
201    pub text: String,
202    pub mapping: LineSpanMapping,
203}
204
205impl LoweredSource {
206    pub fn identity(text: String) -> Self {
207        let mapping = LineSpanMapping::identity(&text);
208        Self { text, mapping }
209    }
210}
211
212fn compute_line_starts(text: &str) -> Vec<usize> {
213    let mut starts = vec![0usize];
214    for (idx, ch) in text.char_indices() {
215        if ch == '\n' {
216            starts.push(idx + 1);
217        }
218    }
219    if starts.is_empty() {
220        starts.push(0);
221    }
222    starts
223}
224
225fn line_index_for_offset(line_starts: &[usize], offset: usize) -> Option<usize> {
226    if line_starts.is_empty() {
227        return None;
228    }
229    let mut lo = 0usize;
230    let mut hi = line_starts.len();
231    while lo < hi {
232        let mid = (lo + hi) / 2;
233        if line_starts[mid] <= offset {
234            lo = mid + 1;
235        } else {
236            hi = mid;
237        }
238    }
239    Some(lo.saturating_sub(1))
240}