use super::types::{LineColumn, Position, SourceId, SourceInfo, SourceKind, SourceMap, Span};
impl SourceMap {
pub fn new() -> Self {
Self {
sources: Vec::new(),
next_id: 0,
}
}
pub fn add_file(&mut self, name: &str, content: String) -> SourceId {
let id = SourceId(self.next_id);
self.next_id += 1;
self.sources.push(SourceInfo {
id,
name: name.to_owned(),
content,
kind: SourceKind::File,
});
id
}
pub fn add_macro_expansion(
&mut self,
macro_name: &str,
site: Span,
content: String,
) -> SourceId {
let id = SourceId(self.next_id);
self.next_id += 1;
self.sources.push(SourceInfo {
id,
name: format!("<macro:{}>", macro_name),
content,
kind: SourceKind::MacroExpansion {
macro_name: macro_name.to_owned(),
expansion_site: site,
},
});
id
}
pub fn get(&self, id: SourceId) -> Option<&SourceInfo> {
self.sources.iter().find(|s| s.id == id)
}
pub fn span_to_position(&self, span: &Span) -> Option<LineColumn> {
let info = self.get(span.source)?;
let content = &info.content;
if span.start > content.len() {
return None;
}
let (line, col) = byte_offset_to_line_col(content, span.start);
Some(LineColumn {
source: span.source,
position: Position::new(line, col),
})
}
pub fn position_to_offset(&self, lc: &LineColumn) -> Option<usize> {
let info = self.get(lc.source)?;
line_col_to_byte_offset(&info.content, lc.position.line, lc.position.col)
}
pub fn span_text(&self, span: &Span) -> Option<&str> {
let info = self.get(span.source)?;
info.content.get(span.start..span.end)
}
pub fn chain_origin(&self, span: &Span) -> Vec<Span> {
let mut chain: Vec<Span> = vec![span.clone()];
let mut current = span.clone();
while let Some(info) = self.get(current.source) {
match &info.kind {
SourceKind::MacroExpansion { expansion_site, .. } => {
chain.push(expansion_site.clone());
current = expansion_site.clone();
}
_ => break,
}
}
chain
}
}
pub fn span_contains(outer: &Span, inner: &Span) -> bool {
outer.source == inner.source && outer.start <= inner.start && inner.end <= outer.end
}
pub fn merge_spans(a: &Span, b: &Span) -> Option<Span> {
if a.source != b.source {
return None;
}
Some(Span {
source: a.source,
start: a.start.min(b.start),
end: a.end.max(b.end),
})
}
pub(super) fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) {
let safe = offset.min(content.len());
let before = &content[..safe];
let line = before.bytes().filter(|&b| b == b'\n').count() as u32;
let col = before.rfind('\n').map(|nl| safe - nl - 1).unwrap_or(safe) as u32;
(line, col)
}
pub(super) fn line_col_to_byte_offset(content: &str, line: u32, col: u32) -> Option<usize> {
let mut current_line = 0u32;
let mut line_start = 0usize;
for (i, b) in content.bytes().enumerate() {
if current_line == line {
let col_offset = line_start + col as usize;
if col_offset <= content.len() {
return Some(col_offset);
} else {
return None;
}
}
if b == b'\n' {
current_line += 1;
line_start = i + 1;
}
}
if current_line == line {
let col_offset = line_start + col as usize;
if col_offset <= content.len() {
return Some(col_offset);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source_map::types::SourceKind;
fn make_map_with_file(content: &str) -> (SourceMap, SourceId) {
let mut sm = SourceMap::new();
let id = sm.add_file("test.lean", content.to_owned());
(sm, id)
}
#[test]
fn test_source_map_new_empty() {
let sm = SourceMap::new();
assert!(sm.sources.is_empty());
assert_eq!(sm.next_id, 0);
}
#[test]
fn test_add_file_assigns_ids_sequentially() {
let mut sm = SourceMap::new();
let a = sm.add_file("a.lean", "content a".into());
let b = sm.add_file("b.lean", "content b".into());
assert_eq!(a, SourceId(0));
assert_eq!(b, SourceId(1));
}
#[test]
fn test_add_file_stores_content() {
let (sm, id) = make_map_with_file("hello world");
let info = sm.get(id).expect("source not found");
assert_eq!(info.content, "hello world");
assert_eq!(info.name, "test.lean");
assert!(matches!(info.kind, SourceKind::File));
}
#[test]
fn test_add_macro_expansion() {
let mut sm = SourceMap::new();
let file_id = sm.add_file("src.lean", "macro!(x)".into());
let site = Span::new(file_id, 0, 9);
let macro_id = sm.add_macro_expansion("macro", site, "expanded code".into());
let info = sm.get(macro_id).expect("macro source not found");
assert!(matches!(
&info.kind,
SourceKind::MacroExpansion { macro_name, .. } if macro_name == "macro"
));
}
#[test]
fn test_get_unknown_id_returns_none() {
let sm = SourceMap::new();
assert!(sm.get(SourceId(999)).is_none());
}
#[test]
fn test_span_to_position_first_char() {
let (sm, id) = make_map_with_file("hello\nworld");
let span = Span::new(id, 0, 1);
let lc = sm.span_to_position(&span).expect("should resolve");
assert_eq!(lc.position.line, 0);
assert_eq!(lc.position.col, 0);
}
#[test]
fn test_span_to_position_second_line() {
let (sm, id) = make_map_with_file("hello\nworld");
let span = Span::new(id, 6, 11); let lc = sm.span_to_position(&span).expect("should resolve");
assert_eq!(lc.position.line, 1);
assert_eq!(lc.position.col, 0);
}
#[test]
fn test_span_to_position_mid_line() {
let (sm, id) = make_map_with_file("abcde\nfghij");
let span = Span::new(id, 3, 4); let lc = sm.span_to_position(&span).expect("should resolve");
assert_eq!(lc.position.line, 0);
assert_eq!(lc.position.col, 3);
}
#[test]
fn test_span_to_position_out_of_range() {
let (sm, id) = make_map_with_file("abc");
let span = Span::new(id, 100, 110);
assert!(sm.span_to_position(&span).is_none());
}
#[test]
fn test_span_to_position_unknown_source() {
let sm = SourceMap::new();
let span = Span::new(SourceId(0), 0, 1);
assert!(sm.span_to_position(&span).is_none());
}
#[test]
fn test_position_to_offset_roundtrip_single_line() {
let (sm, id) = make_map_with_file("hello world");
for col in 0_u32..=10 {
let lc = LineColumn::new(id, 0, col);
let off = sm.position_to_offset(&lc).expect("should resolve");
let lc2 = sm
.span_to_position(&Span::new(id, off, off + 1))
.expect("roundtrip");
assert_eq!(lc2.position.col, col, "col roundtrip failed for {}", col);
}
}
#[test]
fn test_position_to_offset_multi_line() {
let content = "abc\ndef\nghi";
let (sm, id) = make_map_with_file(content);
let lc = LineColumn::new(id, 2, 0);
let off = sm.position_to_offset(&lc).expect("should resolve");
assert_eq!(&content[off..off + 1], "g");
}
#[test]
fn test_position_to_offset_out_of_range() {
let (sm, id) = make_map_with_file("abc");
let lc = LineColumn::new(id, 99, 0);
assert!(sm.position_to_offset(&lc).is_none());
}
#[test]
fn test_span_text_basic() {
let (sm, id) = make_map_with_file("hello world");
let span = Span::new(id, 6, 11);
assert_eq!(sm.span_text(&span), Some("world"));
}
#[test]
fn test_span_text_empty_span() {
let (sm, id) = make_map_with_file("hello");
let span = Span::new(id, 2, 2);
assert_eq!(sm.span_text(&span), Some(""));
}
#[test]
fn test_span_text_out_of_range() {
let (sm, id) = make_map_with_file("hi");
let span = Span::new(id, 1, 100);
assert!(sm.span_text(&span).is_none());
}
#[test]
fn test_span_text_unknown_source() {
let sm = SourceMap::new();
let span = Span::new(SourceId(0), 0, 1);
assert!(sm.span_text(&span).is_none());
}
#[test]
fn test_chain_origin_file_only() {
let (sm, id) = make_map_with_file("source");
let span = Span::new(id, 0, 6);
let chain = sm.chain_origin(&span);
assert_eq!(chain.len(), 1);
assert_eq!(chain[0], span);
}
#[test]
fn test_chain_origin_one_expansion() {
let mut sm = SourceMap::new();
let file_id = sm.add_file("src.lean", "macro!(x)".into());
let site = Span::new(file_id, 0, 9);
let macro_id = sm.add_macro_expansion("macro", site.clone(), "expanded".into());
let macro_span = Span::new(macro_id, 0, 8);
let chain = sm.chain_origin(¯o_span);
assert_eq!(chain.len(), 2);
assert_eq!(chain[0], macro_span);
assert_eq!(chain[1], site);
}
#[test]
fn test_chain_origin_nested_expansions() {
let mut sm = SourceMap::new();
let file_id = sm.add_file("src.lean", "outer!(inner!(x))".into());
let outer_site = Span::new(file_id, 0, 17);
let mid_id = sm.add_macro_expansion("outer", outer_site.clone(), "mid content".into());
let mid_site = Span::new(mid_id, 0, 11);
let inner_id = sm.add_macro_expansion("inner", mid_site.clone(), "inner expanded".into());
let inner_span = Span::new(inner_id, 0, 14);
let chain = sm.chain_origin(&inner_span);
assert_eq!(chain.len(), 3);
assert_eq!(chain[0], inner_span);
assert_eq!(chain[1], mid_site);
assert_eq!(chain[2], outer_site);
}
#[test]
fn test_span_contains_same_source() {
let id = SourceId(0);
let outer = Span::new(id, 0, 10);
let inner = Span::new(id, 2, 8);
assert!(span_contains(&outer, &inner));
}
#[test]
fn test_span_contains_exact() {
let id = SourceId(0);
let span = Span::new(id, 5, 10);
assert!(span_contains(&span, &span));
}
#[test]
fn test_span_contains_not_contained() {
let id = SourceId(0);
let a = Span::new(id, 0, 5);
let b = Span::new(id, 3, 8);
assert!(!span_contains(&a, &b));
}
#[test]
fn test_span_contains_different_sources() {
let a_span = Span::new(SourceId(0), 0, 10);
let b_span = Span::new(SourceId(1), 2, 8);
assert!(!span_contains(&a_span, &b_span));
}
#[test]
fn test_merge_spans_same_source() {
let id = SourceId(0);
let a = Span::new(id, 2, 5);
let b = Span::new(id, 4, 9);
let merged = merge_spans(&a, &b).expect("should merge");
assert_eq!(merged.start, 2);
assert_eq!(merged.end, 9);
}
#[test]
fn test_merge_spans_disjoint() {
let id = SourceId(0);
let a = Span::new(id, 0, 3);
let b = Span::new(id, 7, 10);
let merged = merge_spans(&a, &b).expect("should merge");
assert_eq!(merged.start, 0);
assert_eq!(merged.end, 10);
}
#[test]
fn test_merge_spans_different_sources() {
let a = Span::new(SourceId(0), 0, 5);
let b = Span::new(SourceId(1), 0, 5);
assert!(merge_spans(&a, &b).is_none());
}
#[test]
fn test_merge_spans_identical() {
let id = SourceId(0);
let span = Span::new(id, 3, 7);
let merged = merge_spans(&span, &span).expect("merge");
assert_eq!(merged, span);
}
#[test]
fn test_span_len() {
let span = Span::new(SourceId(0), 4, 9);
assert_eq!(span.len(), 5);
}
#[test]
fn test_span_is_empty() {
let empty = Span::new(SourceId(0), 5, 5);
let non_empty = Span::new(SourceId(0), 5, 6);
assert!(empty.is_empty());
assert!(!non_empty.is_empty());
}
#[test]
fn test_span_chain_depth() {
let mut chain = crate::source_map::types::SpanChain::new();
assert_eq!(chain.depth(), 0);
chain.push(Span::new(SourceId(0), 0, 1));
assert_eq!(chain.depth(), 1);
}
#[test]
fn test_span_chain_outermost_innermost() {
let mut chain = crate::source_map::types::SpanChain::new();
let a = Span::new(SourceId(0), 0, 1);
let b = Span::new(SourceId(1), 2, 3);
chain.push(a.clone());
chain.push(b.clone());
assert_eq!(chain.outermost(), Some(&a));
assert_eq!(chain.innermost(), Some(&b));
}
#[test]
fn test_offset_line_col_roundtrip() {
let content = "abc\ndef\nghi";
for off in 0..content.len() {
let (line, col) = byte_offset_to_line_col(content, off);
let back = line_col_to_byte_offset(content, line, col).expect("roundtrip");
assert_eq!(back, off, "roundtrip failed for offset {}", off);
}
}
#[test]
fn test_position_display_1_based() {
let p = Position::new(0, 0);
assert_eq!(format!("{}", p), "1:1");
let p2 = Position::new(2, 4);
assert_eq!(format!("{}", p2), "3:5");
}
}