vm/compiler/
source_map.rs1use 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}