use crate::span::Span;
#[derive(Debug, Clone)]
pub struct SourceFile<'a> {
name: String,
source: &'a str,
start_offset: usize,
end_offset: usize,
first_imported_at: Option<Span>,
}
impl SourceFile<'_> {
pub fn name(&self) -> &str {
&self.name
}
pub fn source(&self) -> &str {
self.source
}
pub fn start_offset(&self) -> usize {
self.start_offset
}
pub fn end_offset(&self) -> usize {
self.end_offset
}
pub fn first_imported_at(&self) -> Option<Span> {
self.first_imported_at
}
pub fn len(&self) -> usize {
self.source.len()
}
pub fn is_empty(&self) -> bool {
self.source.is_empty()
}
}
#[derive(Debug, Default)]
pub struct SourceMap<'a> {
files: Vec<SourceFile<'a>>,
next_offset: usize,
}
impl<'a> SourceMap<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn add_file(
&mut self,
name: impl Into<String>,
source: &'a str,
imported_at: Option<Span>,
) -> usize {
let start_offset = self.next_offset;
let end_offset = start_offset + source.len();
self.files.push(SourceFile {
name: name.into(),
source,
start_offset,
end_offset,
first_imported_at: imported_at,
});
self.next_offset = end_offset + 1;
start_offset
}
pub fn lookup_file(&self, offset: usize) -> Option<&SourceFile<'a>> {
let idx = self.files.partition_point(|f| f.start_offset <= offset);
if idx == 0 {
return None;
}
let file = &self.files[idx - 1];
if offset < file.end_offset {
Some(file)
} else {
None }
}
pub fn lookup_file_by_span(&self, span: Span) -> Option<&SourceFile<'a>> {
let file = self.lookup_file(span.start())?;
if span.end() <= file.end_offset {
Some(file)
} else {
None
}
}
pub fn source_slice(&self, span: Span) -> Option<&str> {
let file = self.lookup_file_by_span(span)?;
let local_start = span.start() - file.start_offset;
let local_end = span.end() - file.start_offset;
Some(&file.source[local_start..local_end])
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn files(&self) -> &[SourceFile<'a>] {
&self.files
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_single_file_returns_zero_offset() {
let mut map = SourceMap::new();
let offset = map.add_file("a.orr", "hello", None);
assert_eq!(offset, 0);
assert_eq!(map.file_count(), 1);
}
#[test]
fn add_multiple_files_with_gaps() {
let mut map = SourceMap::new();
let a = map.add_file("a.orr", "hello", None);
assert_eq!(a, 0);
let b = map.add_file("b.orr", "world", None);
assert_eq!(b, 6);
let c = map.add_file("c.orr", "foo", None);
assert_eq!(c, 12);
assert_eq!(map.file_count(), 3);
}
#[test]
fn add_empty_file() {
let mut map = SourceMap::new();
let a = map.add_file("empty.orr", "", None);
assert_eq!(a, 0);
let b = map.add_file("b.orr", "x", None);
assert_eq!(b, 1);
}
#[test]
fn add_file_records_imported_at_span() {
let mut map = SourceMap::new();
let import_span = Span::new(10..25);
map.add_file("root.orr", "root", None);
map.add_file("lib.orr", "lib", Some(import_span));
assert!(map.files()[0].first_imported_at().is_none());
assert_eq!(map.files()[1].first_imported_at(), Some(import_span));
}
#[test]
fn source_file_accessors() {
let mut map = SourceMap::new();
map.add_file("test.orr", "abc", None);
let file = &map.files()[0];
assert_eq!(file.name(), "test.orr");
assert_eq!(file.source(), "abc");
assert_eq!(file.start_offset(), 0);
assert_eq!(file.end_offset(), 3);
assert_eq!(file.len(), 3);
assert!(!file.is_empty());
}
#[test]
fn empty_source_file() {
let mut map = SourceMap::new();
map.add_file("empty.orr", "", None);
let file = &map.files()[0];
assert_eq!(file.len(), 0);
assert!(file.is_empty());
assert_eq!(file.start_offset(), file.end_offset());
}
#[test]
fn lookup_file_single() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None);
for i in 0..5 {
let file = map.lookup_file(i);
assert!(file.is_some(), "offset {i} should resolve");
assert_eq!(file.unwrap().name(), "a.orr");
}
assert!(map.lookup_file(5).is_none());
assert!(map.lookup_file(100).is_none());
}
#[test]
fn lookup_file_multiple() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None); map.add_file("b.orr", "world", None); map.add_file("c.orr", "foo", None);
assert_eq!(map.lookup_file(0).unwrap().name(), "a.orr");
assert_eq!(map.lookup_file(4).unwrap().name(), "a.orr");
assert!(map.lookup_file(5).is_none()); assert_eq!(map.lookup_file(6).unwrap().name(), "b.orr");
assert_eq!(map.lookup_file(10).unwrap().name(), "b.orr");
assert!(map.lookup_file(11).is_none()); assert_eq!(map.lookup_file(12).unwrap().name(), "c.orr");
assert_eq!(map.lookup_file(14).unwrap().name(), "c.orr");
assert!(map.lookup_file(15).is_none()); }
#[test]
fn lookup_file_empty_map() {
let map = SourceMap::new();
assert!(map.lookup_file(0).is_none());
}
#[test]
fn lookup_file_empty_file_returns_none() {
let mut map = SourceMap::new();
map.add_file("empty.orr", "", None);
assert!(map.lookup_file(0).is_none());
}
#[test]
fn source_slice_within_file() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello world", None);
let slice = map.source_slice(Span::new(0..5)).unwrap();
assert_eq!(slice, "hello");
let slice = map.source_slice(Span::new(6..11)).unwrap();
assert_eq!(slice, "world");
}
#[test]
fn source_slice_entire_file() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None);
let slice = map.source_slice(Span::new(0..5)).unwrap();
assert_eq!(slice, "hello");
}
#[test]
fn source_slice_empty_span() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None);
let slice = map.source_slice(Span::new(2..2)).unwrap();
assert_eq!(slice, "");
}
#[test]
fn source_slice_second_file() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None); map.add_file("b.orr", "world", None);
let slice = map.source_slice(Span::new(6..11)).unwrap();
assert_eq!(slice, "world");
let slice = map.source_slice(Span::new(8..11)).unwrap();
assert_eq!(slice, "rld");
}
#[test]
fn source_slice_crossing_files_returns_none() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None); map.add_file("b.orr", "world", None);
assert!(map.source_slice(Span::new(3..6)).is_none());
assert!(map.source_slice(Span::new(3..8)).is_none());
}
#[test]
fn source_slice_in_gap_returns_none() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None); map.add_file("b.orr", "world", None);
assert!(map.source_slice(Span::new(5..6)).is_none());
}
#[test]
fn source_slice_out_of_range_returns_none() {
let mut map = SourceMap::new();
map.add_file("a.orr", "hello", None);
assert!(map.source_slice(Span::new(100..105)).is_none());
}
#[test]
fn virtual_address_space_layout() {
let source_a = "x".repeat(100);
let source_b = "y".repeat(80);
let source_c = "z".repeat(50);
let mut map = SourceMap::new();
let a = map.add_file("a.orr", &source_a, None);
assert_eq!(a, 0);
assert_eq!(map.files()[0].start_offset(), 0);
assert_eq!(map.files()[0].end_offset(), 100);
let b = map.add_file("b.orr", &source_b, None);
assert_eq!(b, 101);
assert_eq!(map.files()[1].start_offset(), 101);
assert_eq!(map.files()[1].end_offset(), 181);
let c = map.add_file("c.orr", &source_c, None);
assert_eq!(c, 182);
assert_eq!(map.files()[2].start_offset(), 182);
assert_eq!(map.files()[2].end_offset(), 232);
assert!(map.lookup_file(100).is_none());
assert!(map.lookup_file(181).is_none());
assert_eq!(map.lookup_file(99).unwrap().name(), "a.orr");
assert_eq!(map.lookup_file(180).unwrap().name(), "b.orr");
assert_eq!(map.lookup_file(231).unwrap().name(), "c.orr");
assert!(map.lookup_file(232).is_none());
}
}