1use super::types::{LineColumn, Position, SourceId, SourceInfo, SourceKind, SourceMap, Span};
4
5impl SourceMap {
8 pub fn new() -> Self {
10 Self {
11 sources: Vec::new(),
12 next_id: 0,
13 }
14 }
15
16 pub fn add_file(&mut self, name: &str, content: String) -> SourceId {
18 let id = SourceId(self.next_id);
19 self.next_id += 1;
20 self.sources.push(SourceInfo {
21 id,
22 name: name.to_owned(),
23 content,
24 kind: SourceKind::File,
25 });
26 id
27 }
28
29 pub fn add_macro_expansion(
31 &mut self,
32 macro_name: &str,
33 site: Span,
34 content: String,
35 ) -> SourceId {
36 let id = SourceId(self.next_id);
37 self.next_id += 1;
38 self.sources.push(SourceInfo {
39 id,
40 name: format!("<macro:{}>", macro_name),
41 content,
42 kind: SourceKind::MacroExpansion {
43 macro_name: macro_name.to_owned(),
44 expansion_site: site,
45 },
46 });
47 id
48 }
49
50 pub fn get(&self, id: SourceId) -> Option<&SourceInfo> {
52 self.sources.iter().find(|s| s.id == id)
53 }
54
55 pub fn span_to_position(&self, span: &Span) -> Option<LineColumn> {
60 let info = self.get(span.source)?;
61 let content = &info.content;
62 if span.start > content.len() {
63 return None;
64 }
65 let (line, col) = byte_offset_to_line_col(content, span.start);
66 Some(LineColumn {
67 source: span.source,
68 position: Position::new(line, col),
69 })
70 }
71
72 pub fn position_to_offset(&self, lc: &LineColumn) -> Option<usize> {
76 let info = self.get(lc.source)?;
77 line_col_to_byte_offset(&info.content, lc.position.line, lc.position.col)
78 }
79
80 pub fn span_text(&self, span: &Span) -> Option<&str> {
84 let info = self.get(span.source)?;
85 info.content.get(span.start..span.end)
86 }
87
88 pub fn chain_origin(&self, span: &Span) -> Vec<Span> {
95 let mut chain: Vec<Span> = vec![span.clone()];
96 let mut current = span.clone();
97
98 while let Some(info) = self.get(current.source) {
99 match &info.kind {
100 SourceKind::MacroExpansion { expansion_site, .. } => {
101 chain.push(expansion_site.clone());
102 current = expansion_site.clone();
103 }
104 _ => break,
105 }
106 }
107
108 chain
109 }
110}
111
112pub fn span_contains(outer: &Span, inner: &Span) -> bool {
118 outer.source == inner.source && outer.start <= inner.start && inner.end <= outer.end
119}
120
121pub fn merge_spans(a: &Span, b: &Span) -> Option<Span> {
125 if a.source != b.source {
126 return None;
127 }
128 Some(Span {
129 source: a.source,
130 start: a.start.min(b.start),
131 end: a.end.max(b.end),
132 })
133}
134
135pub(super) fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) {
139 let safe = offset.min(content.len());
140 let before = &content[..safe];
141 let line = before.bytes().filter(|&b| b == b'\n').count() as u32;
142 let col = before.rfind('\n').map(|nl| safe - nl - 1).unwrap_or(safe) as u32;
143 (line, col)
144}
145
146pub(super) fn line_col_to_byte_offset(content: &str, line: u32, col: u32) -> Option<usize> {
150 let mut current_line = 0u32;
151 let mut line_start = 0usize;
152
153 for (i, b) in content.bytes().enumerate() {
154 if current_line == line {
155 let col_offset = line_start + col as usize;
156 if col_offset <= content.len() {
157 return Some(col_offset);
158 } else {
159 return None;
160 }
161 }
162 if b == b'\n' {
163 current_line += 1;
164 line_start = i + 1;
165 }
166 }
167
168 if current_line == line {
170 let col_offset = line_start + col as usize;
171 if col_offset <= content.len() {
172 return Some(col_offset);
173 }
174 }
175
176 None
177}
178
179#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::source_map::types::SourceKind;
185
186 fn make_map_with_file(content: &str) -> (SourceMap, SourceId) {
187 let mut sm = SourceMap::new();
188 let id = sm.add_file("test.lean", content.to_owned());
189 (sm, id)
190 }
191
192 #[test]
195 fn test_source_map_new_empty() {
196 let sm = SourceMap::new();
197 assert!(sm.sources.is_empty());
198 assert_eq!(sm.next_id, 0);
199 }
200
201 #[test]
204 fn test_add_file_assigns_ids_sequentially() {
205 let mut sm = SourceMap::new();
206 let a = sm.add_file("a.lean", "content a".into());
207 let b = sm.add_file("b.lean", "content b".into());
208 assert_eq!(a, SourceId(0));
209 assert_eq!(b, SourceId(1));
210 }
211
212 #[test]
213 fn test_add_file_stores_content() {
214 let (sm, id) = make_map_with_file("hello world");
215 let info = sm.get(id).expect("source not found");
216 assert_eq!(info.content, "hello world");
217 assert_eq!(info.name, "test.lean");
218 assert!(matches!(info.kind, SourceKind::File));
219 }
220
221 #[test]
224 fn test_add_macro_expansion() {
225 let mut sm = SourceMap::new();
226 let file_id = sm.add_file("src.lean", "macro!(x)".into());
227 let site = Span::new(file_id, 0, 9);
228 let macro_id = sm.add_macro_expansion("macro", site, "expanded code".into());
229 let info = sm.get(macro_id).expect("macro source not found");
230 assert!(matches!(
231 &info.kind,
232 SourceKind::MacroExpansion { macro_name, .. } if macro_name == "macro"
233 ));
234 }
235
236 #[test]
239 fn test_get_unknown_id_returns_none() {
240 let sm = SourceMap::new();
241 assert!(sm.get(SourceId(999)).is_none());
242 }
243
244 #[test]
247 fn test_span_to_position_first_char() {
248 let (sm, id) = make_map_with_file("hello\nworld");
249 let span = Span::new(id, 0, 1);
250 let lc = sm.span_to_position(&span).expect("should resolve");
251 assert_eq!(lc.position.line, 0);
252 assert_eq!(lc.position.col, 0);
253 }
254
255 #[test]
256 fn test_span_to_position_second_line() {
257 let (sm, id) = make_map_with_file("hello\nworld");
258 let span = Span::new(id, 6, 11); let lc = sm.span_to_position(&span).expect("should resolve");
260 assert_eq!(lc.position.line, 1);
261 assert_eq!(lc.position.col, 0);
262 }
263
264 #[test]
265 fn test_span_to_position_mid_line() {
266 let (sm, id) = make_map_with_file("abcde\nfghij");
267 let span = Span::new(id, 3, 4); let lc = sm.span_to_position(&span).expect("should resolve");
269 assert_eq!(lc.position.line, 0);
270 assert_eq!(lc.position.col, 3);
271 }
272
273 #[test]
274 fn test_span_to_position_out_of_range() {
275 let (sm, id) = make_map_with_file("abc");
276 let span = Span::new(id, 100, 110);
277 assert!(sm.span_to_position(&span).is_none());
278 }
279
280 #[test]
281 fn test_span_to_position_unknown_source() {
282 let sm = SourceMap::new();
283 let span = Span::new(SourceId(0), 0, 1);
284 assert!(sm.span_to_position(&span).is_none());
285 }
286
287 #[test]
290 fn test_position_to_offset_roundtrip_single_line() {
291 let (sm, id) = make_map_with_file("hello world");
292 for col in 0_u32..=10 {
293 let lc = LineColumn::new(id, 0, col);
294 let off = sm.position_to_offset(&lc).expect("should resolve");
295 let lc2 = sm
296 .span_to_position(&Span::new(id, off, off + 1))
297 .expect("roundtrip");
298 assert_eq!(lc2.position.col, col, "col roundtrip failed for {}", col);
299 }
300 }
301
302 #[test]
303 fn test_position_to_offset_multi_line() {
304 let content = "abc\ndef\nghi";
305 let (sm, id) = make_map_with_file(content);
306 let lc = LineColumn::new(id, 2, 0);
308 let off = sm.position_to_offset(&lc).expect("should resolve");
309 assert_eq!(&content[off..off + 1], "g");
310 }
311
312 #[test]
313 fn test_position_to_offset_out_of_range() {
314 let (sm, id) = make_map_with_file("abc");
315 let lc = LineColumn::new(id, 99, 0);
316 assert!(sm.position_to_offset(&lc).is_none());
317 }
318
319 #[test]
322 fn test_span_text_basic() {
323 let (sm, id) = make_map_with_file("hello world");
324 let span = Span::new(id, 6, 11);
325 assert_eq!(sm.span_text(&span), Some("world"));
326 }
327
328 #[test]
329 fn test_span_text_empty_span() {
330 let (sm, id) = make_map_with_file("hello");
331 let span = Span::new(id, 2, 2);
332 assert_eq!(sm.span_text(&span), Some(""));
333 }
334
335 #[test]
336 fn test_span_text_out_of_range() {
337 let (sm, id) = make_map_with_file("hi");
338 let span = Span::new(id, 1, 100);
339 assert!(sm.span_text(&span).is_none());
340 }
341
342 #[test]
343 fn test_span_text_unknown_source() {
344 let sm = SourceMap::new();
345 let span = Span::new(SourceId(0), 0, 1);
346 assert!(sm.span_text(&span).is_none());
347 }
348
349 #[test]
352 fn test_chain_origin_file_only() {
353 let (sm, id) = make_map_with_file("source");
354 let span = Span::new(id, 0, 6);
355 let chain = sm.chain_origin(&span);
356 assert_eq!(chain.len(), 1);
357 assert_eq!(chain[0], span);
358 }
359
360 #[test]
361 fn test_chain_origin_one_expansion() {
362 let mut sm = SourceMap::new();
363 let file_id = sm.add_file("src.lean", "macro!(x)".into());
364 let site = Span::new(file_id, 0, 9);
365 let macro_id = sm.add_macro_expansion("macro", site.clone(), "expanded".into());
366 let macro_span = Span::new(macro_id, 0, 8);
367 let chain = sm.chain_origin(¯o_span);
368 assert_eq!(chain.len(), 2);
370 assert_eq!(chain[0], macro_span);
371 assert_eq!(chain[1], site);
372 }
373
374 #[test]
375 fn test_chain_origin_nested_expansions() {
376 let mut sm = SourceMap::new();
377 let file_id = sm.add_file("src.lean", "outer!(inner!(x))".into());
378 let outer_site = Span::new(file_id, 0, 17);
379 let mid_id = sm.add_macro_expansion("outer", outer_site.clone(), "mid content".into());
380 let mid_site = Span::new(mid_id, 0, 11);
381 let inner_id = sm.add_macro_expansion("inner", mid_site.clone(), "inner expanded".into());
382 let inner_span = Span::new(inner_id, 0, 14);
383
384 let chain = sm.chain_origin(&inner_span);
385 assert_eq!(chain.len(), 3);
386 assert_eq!(chain[0], inner_span);
387 assert_eq!(chain[1], mid_site);
388 assert_eq!(chain[2], outer_site);
389 }
390
391 #[test]
394 fn test_span_contains_same_source() {
395 let id = SourceId(0);
396 let outer = Span::new(id, 0, 10);
397 let inner = Span::new(id, 2, 8);
398 assert!(span_contains(&outer, &inner));
399 }
400
401 #[test]
402 fn test_span_contains_exact() {
403 let id = SourceId(0);
404 let span = Span::new(id, 5, 10);
405 assert!(span_contains(&span, &span));
406 }
407
408 #[test]
409 fn test_span_contains_not_contained() {
410 let id = SourceId(0);
411 let a = Span::new(id, 0, 5);
412 let b = Span::new(id, 3, 8);
413 assert!(!span_contains(&a, &b));
414 }
415
416 #[test]
417 fn test_span_contains_different_sources() {
418 let a_span = Span::new(SourceId(0), 0, 10);
419 let b_span = Span::new(SourceId(1), 2, 8);
420 assert!(!span_contains(&a_span, &b_span));
421 }
422
423 #[test]
426 fn test_merge_spans_same_source() {
427 let id = SourceId(0);
428 let a = Span::new(id, 2, 5);
429 let b = Span::new(id, 4, 9);
430 let merged = merge_spans(&a, &b).expect("should merge");
431 assert_eq!(merged.start, 2);
432 assert_eq!(merged.end, 9);
433 }
434
435 #[test]
436 fn test_merge_spans_disjoint() {
437 let id = SourceId(0);
438 let a = Span::new(id, 0, 3);
439 let b = Span::new(id, 7, 10);
440 let merged = merge_spans(&a, &b).expect("should merge");
441 assert_eq!(merged.start, 0);
442 assert_eq!(merged.end, 10);
443 }
444
445 #[test]
446 fn test_merge_spans_different_sources() {
447 let a = Span::new(SourceId(0), 0, 5);
448 let b = Span::new(SourceId(1), 0, 5);
449 assert!(merge_spans(&a, &b).is_none());
450 }
451
452 #[test]
453 fn test_merge_spans_identical() {
454 let id = SourceId(0);
455 let span = Span::new(id, 3, 7);
456 let merged = merge_spans(&span, &span).expect("merge");
457 assert_eq!(merged, span);
458 }
459
460 #[test]
463 fn test_span_len() {
464 let span = Span::new(SourceId(0), 4, 9);
465 assert_eq!(span.len(), 5);
466 }
467
468 #[test]
469 fn test_span_is_empty() {
470 let empty = Span::new(SourceId(0), 5, 5);
471 let non_empty = Span::new(SourceId(0), 5, 6);
472 assert!(empty.is_empty());
473 assert!(!non_empty.is_empty());
474 }
475
476 #[test]
479 fn test_span_chain_depth() {
480 let mut chain = crate::source_map::types::SpanChain::new();
481 assert_eq!(chain.depth(), 0);
482 chain.push(Span::new(SourceId(0), 0, 1));
483 assert_eq!(chain.depth(), 1);
484 }
485
486 #[test]
487 fn test_span_chain_outermost_innermost() {
488 let mut chain = crate::source_map::types::SpanChain::new();
489 let a = Span::new(SourceId(0), 0, 1);
490 let b = Span::new(SourceId(1), 2, 3);
491 chain.push(a.clone());
492 chain.push(b.clone());
493 assert_eq!(chain.outermost(), Some(&a));
494 assert_eq!(chain.innermost(), Some(&b));
495 }
496
497 #[test]
500 fn test_offset_line_col_roundtrip() {
501 let content = "abc\ndef\nghi";
502 for off in 0..content.len() {
503 let (line, col) = byte_offset_to_line_col(content, off);
504 let back = line_col_to_byte_offset(content, line, col).expect("roundtrip");
505 assert_eq!(back, off, "roundtrip failed for offset {}", off);
506 }
507 }
508
509 #[test]
512 fn test_position_display_1_based() {
513 let p = Position::new(0, 0);
514 assert_eq!(format!("{}", p), "1:1");
515 let p2 = Position::new(2, 4);
516 assert_eq!(format!("{}", p2), "3:5");
517 }
518}