Skip to main content

oxilean_parse/source_map/
functions.rs

1//! Functions for source map construction and querying.
2
3use super::types::{LineColumn, Position, SourceId, SourceInfo, SourceKind, SourceMap, Span};
4
5// ── SourceMap impl ───────────────────────────────────────────────────────────
6
7impl SourceMap {
8    /// Create an empty `SourceMap`.
9    pub fn new() -> Self {
10        Self {
11            sources: Vec::new(),
12            next_id: 0,
13        }
14    }
15
16    /// Register a source file and return its [`SourceId`].
17    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    /// Register a macro expansion and return its [`SourceId`].
30    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    /// Look up a source by its identifier.
51    pub fn get(&self, id: SourceId) -> Option<&SourceInfo> {
52        self.sources.iter().find(|s| s.id == id)
53    }
54
55    /// Convert a byte-offset span to a [`LineColumn`].
56    ///
57    /// Returns the position of the *start* of the span.  Returns `None` if
58    /// the source is unknown or the offset is out of range.
59    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    /// Convert a [`LineColumn`] back to a byte offset.
73    ///
74    /// Returns `None` if the source is unknown or the position is out of range.
75    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    /// Return the text slice covered by `span`.
81    ///
82    /// Returns `None` if the source is unknown or the span is out of range.
83    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    /// Walk back through macro expansion sites and collect origin spans.
89    ///
90    /// If `span` belongs to a macro-expanded source, this function follows the
91    /// expansion chain until it reaches a non-expanded source.  The returned
92    /// vector goes from `span` (innermost) to the original user-written span
93    /// (outermost).
94    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
112// ── Free span utilities ──────────────────────────────────────────────────────
113
114/// Returns `true` if `inner` is entirely contained within `outer`.
115///
116/// Both spans must reference the same [`SourceId`] for this to return `true`.
117pub fn span_contains(outer: &Span, inner: &Span) -> bool {
118    outer.source == inner.source && outer.start <= inner.start && inner.end <= outer.end
119}
120
121/// Merge two spans into the smallest span that contains both.
122///
123/// Returns `None` if the spans belong to different sources.
124pub 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
135// ── Internal helpers ─────────────────────────────────────────────────────────
136
137/// Convert a byte offset in `content` to a 0-based `(line, col)` pair.
138pub(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
146/// Convert a 0-based `(line, col)` pair to a byte offset in `content`.
147///
148/// Returns `None` when the position is out of range.
149pub(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    // Handle last line (no trailing newline)
169    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// ── Tests ────────────────────────────────────────────────────────────────────
180
181#[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    // ── SourceMap::new ───────────────────────────────────────────────────────
193
194    #[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    // ── add_file ────────────────────────────────────────────────────────────
202
203    #[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    // ── add_macro_expansion ─────────────────────────────────────────────────
222
223    #[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    // ── get ─────────────────────────────────────────────────────────────────
237
238    #[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    // ── span_to_position ────────────────────────────────────────────────────
245
246    #[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); // 'world'
259        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); // 'd'
268        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    // ── position_to_offset ──────────────────────────────────────────────────
288
289    #[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        // 'g' is at line 2, col 0, byte offset 8
307        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    // ── span_text ───────────────────────────────────────────────────────────
320
321    #[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    // ── chain_origin ────────────────────────────────────────────────────────
350
351    #[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(&macro_span);
368        // Should have the macro span and the expansion site
369        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    // ── span_contains ───────────────────────────────────────────────────────
392
393    #[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    // ── merge_spans ─────────────────────────────────────────────────────────
424
425    #[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    // ── Span helpers ─────────────────────────────────────────────────────────
461
462    #[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    // ── SpanChain helpers ────────────────────────────────────────────────────
477
478    #[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    // ── byte_offset_to_line_col / line_col_to_byte_offset roundtrip ─────────
498
499    #[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    // ── Position display ─────────────────────────────────────────────────────
510
511    #[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}