| use std::ops::Range; | |
| use std::sync::Arc; | |
| use crate::source_map::SourceMap; | |
| use crate::{BytePos, Pos, RelativeBytePos, SourceFile, SpanData}; | |
| #[derive(Clone)] | |
| struct CacheEntry { | |
| time_stamp: usize, | |
| line_number: usize, | |
| // The line's byte position range in the `SourceMap`. This range will fail to contain a valid | |
| // position in certain edge cases. Spans often start/end one past something, and when that | |
| // something is the last character of a file (this can happen when a file doesn't end in a | |
| // newline, for example), we'd still like for the position to be considered within the last | |
| // line. However, it isn't according to the exclusive upper bound of this range. We cannot | |
| // change the upper bound to be inclusive, because for most lines, the upper bound is the same | |
| // as the lower bound of the next line, so there would be an ambiguity. | |
| // | |
| // Since the containment aspect of this range is only used to see whether or not the cache | |
| // entry contains a position, the only ramification of the above is that we will get cache | |
| // misses for these rare positions. A line lookup for the position via `SourceMap::lookup_line` | |
| // after a cache miss will produce the last line number, as desired. | |
| line: Range<BytePos>, | |
| file: Arc<SourceFile>, | |
| file_index: usize, | |
| } | |
| impl CacheEntry { | |
| #[inline] | |
| fn update( | |
| &mut self, | |
| new_file_and_idx: Option<(Arc<SourceFile>, usize)>, | |
| pos: BytePos, | |
| time_stamp: usize, | |
| ) { | |
| if let Some((file, file_idx)) = new_file_and_idx { | |
| self.file = file; | |
| self.file_index = file_idx; | |
| } | |
| let pos = self.file.relative_position(pos); | |
| let line_index = self.file.lookup_line(pos).unwrap(); | |
| let line_bounds = self.file.line_bounds(line_index); | |
| self.line_number = line_index + 1; | |
| self.line = line_bounds; | |
| self.touch(time_stamp); | |
| } | |
| #[inline] | |
| fn touch(&mut self, time_stamp: usize) { | |
| self.time_stamp = time_stamp; | |
| } | |
| } | |
| #[derive(Clone)] | |
| pub struct CachingSourceMapView<'sm> { | |
| source_map: &'sm SourceMap, | |
| line_cache: [CacheEntry; 3], | |
| time_stamp: usize, | |
| } | |
| impl<'sm> CachingSourceMapView<'sm> { | |
| pub fn new(source_map: &'sm SourceMap) -> CachingSourceMapView<'sm> { | |
| let files = source_map.files(); | |
| let first_file = Arc::clone(&files[0]); | |
| let entry = CacheEntry { | |
| time_stamp: 0, | |
| line_number: 0, | |
| line: BytePos(0)..BytePos(0), | |
| file: first_file, | |
| file_index: 0, | |
| }; | |
| CachingSourceMapView { | |
| source_map, | |
| line_cache: [entry.clone(), entry.clone(), entry], | |
| time_stamp: 0, | |
| } | |
| } | |
| pub fn byte_pos_to_line_and_col( | |
| &mut self, | |
| pos: BytePos, | |
| ) -> Option<(Arc<SourceFile>, usize, RelativeBytePos)> { | |
| self.time_stamp += 1; | |
| // Check if the position is in one of the cached lines | |
| let cache_idx = self.cache_entry_index(pos); | |
| if cache_idx != -1 { | |
| let cache_entry = &mut self.line_cache[cache_idx as usize]; | |
| cache_entry.touch(self.time_stamp); | |
| let col = RelativeBytePos(pos.to_u32() - cache_entry.line.start.to_u32()); | |
| return Some((Arc::clone(&cache_entry.file), cache_entry.line_number, col)); | |
| } | |
| // No cache hit ... | |
| let oldest = self.oldest_cache_entry_index(); | |
| // If the entry doesn't point to the correct file, get the new file and index. | |
| let new_file_and_idx = if !file_contains(&self.line_cache[oldest].file, pos) { | |
| Some(self.file_for_position(pos)?) | |
| } else { | |
| None | |
| }; | |
| let cache_entry = &mut self.line_cache[oldest]; | |
| cache_entry.update(new_file_and_idx, pos, self.time_stamp); | |
| let col = RelativeBytePos(pos.to_u32() - cache_entry.line.start.to_u32()); | |
| Some((Arc::clone(&cache_entry.file), cache_entry.line_number, col)) | |
| } | |
| pub fn span_data_to_lines_and_cols( | |
| &mut self, | |
| span_data: &SpanData, | |
| ) -> Option<(&SourceFile, usize, BytePos, usize, BytePos)> { | |
| self.time_stamp += 1; | |
| // Check if lo and hi are in the cached lines. | |
| let lo_cache_idx: isize = self.cache_entry_index(span_data.lo); | |
| let hi_cache_idx = self.cache_entry_index(span_data.hi); | |
| if lo_cache_idx != -1 && hi_cache_idx != -1 { | |
| // Cache hit for span lo and hi. Check if they belong to the same file. | |
| let lo_file_index = self.line_cache[lo_cache_idx as usize].file_index; | |
| let hi_file_index = self.line_cache[hi_cache_idx as usize].file_index; | |
| if lo_file_index != hi_file_index { | |
| return None; | |
| } | |
| self.line_cache[lo_cache_idx as usize].touch(self.time_stamp); | |
| self.line_cache[hi_cache_idx as usize].touch(self.time_stamp); | |
| let lo = &self.line_cache[lo_cache_idx as usize]; | |
| let hi = &self.line_cache[hi_cache_idx as usize]; | |
| return Some(( | |
| &lo.file, | |
| lo.line_number, | |
| span_data.lo - lo.line.start, | |
| hi.line_number, | |
| span_data.hi - hi.line.start, | |
| )); | |
| } | |
| // No cache hit or cache hit for only one of span lo and hi. | |
| let oldest = if lo_cache_idx != -1 || hi_cache_idx != -1 { | |
| let avoid_idx = if lo_cache_idx != -1 { lo_cache_idx } else { hi_cache_idx }; | |
| self.oldest_cache_entry_index_avoid(avoid_idx as usize) | |
| } else { | |
| self.oldest_cache_entry_index() | |
| }; | |
| // If the entry doesn't point to the correct file, get the new file and index. | |
| // Return early if the file containing beginning of span doesn't contain end of span. | |
| let new_file_and_idx = if !file_contains(&self.line_cache[oldest].file, span_data.lo) { | |
| let new_file_and_idx = self.file_for_position(span_data.lo)?; | |
| if !file_contains(&new_file_and_idx.0, span_data.hi) { | |
| return None; | |
| } | |
| Some(new_file_and_idx) | |
| } else { | |
| let file = &self.line_cache[oldest].file; | |
| if !file_contains(file, span_data.hi) { | |
| return None; | |
| } | |
| None | |
| }; | |
| // Update the cache entries. | |
| let (lo_idx, hi_idx) = match (lo_cache_idx, hi_cache_idx) { | |
| // Oldest cache entry is for span_data.lo line. | |
| (-1, -1) => { | |
| let lo = &mut self.line_cache[oldest]; | |
| lo.update(new_file_and_idx, span_data.lo, self.time_stamp); | |
| if !lo.line.contains(&span_data.hi) { | |
| let new_file_and_idx = Some((Arc::clone(&lo.file), lo.file_index)); | |
| let next_oldest = self.oldest_cache_entry_index_avoid(oldest); | |
| let hi = &mut self.line_cache[next_oldest]; | |
| hi.update(new_file_and_idx, span_data.hi, self.time_stamp); | |
| (oldest, next_oldest) | |
| } else { | |
| (oldest, oldest) | |
| } | |
| } | |
| // Oldest cache entry is for span_data.lo line. | |
| (-1, _) => { | |
| let lo = &mut self.line_cache[oldest]; | |
| lo.update(new_file_and_idx, span_data.lo, self.time_stamp); | |
| let hi = &mut self.line_cache[hi_cache_idx as usize]; | |
| hi.touch(self.time_stamp); | |
| (oldest, hi_cache_idx as usize) | |
| } | |
| // Oldest cache entry is for span_data.hi line. | |
| (_, -1) => { | |
| let hi = &mut self.line_cache[oldest]; | |
| hi.update(new_file_and_idx, span_data.hi, self.time_stamp); | |
| let lo = &mut self.line_cache[lo_cache_idx as usize]; | |
| lo.touch(self.time_stamp); | |
| (lo_cache_idx as usize, oldest) | |
| } | |
| _ => { | |
| panic!( | |
| "the case of neither value being equal to -1 was handled above and the function returns." | |
| ); | |
| } | |
| }; | |
| let lo = &self.line_cache[lo_idx]; | |
| let hi = &self.line_cache[hi_idx]; | |
| // Span lo and hi may equal line end when last line doesn't | |
| // end in newline, hence the inclusive upper bounds below. | |
| assert!(span_data.lo >= lo.line.start); | |
| assert!(span_data.lo <= lo.line.end); | |
| assert!(span_data.hi >= hi.line.start); | |
| assert!(span_data.hi <= hi.line.end); | |
| assert!(lo.file.contains(span_data.lo)); | |
| assert!(lo.file.contains(span_data.hi)); | |
| assert_eq!(lo.file_index, hi.file_index); | |
| Some(( | |
| &lo.file, | |
| lo.line_number, | |
| span_data.lo - lo.line.start, | |
| hi.line_number, | |
| span_data.hi - hi.line.start, | |
| )) | |
| } | |
| fn cache_entry_index(&self, pos: BytePos) -> isize { | |
| for (idx, cache_entry) in self.line_cache.iter().enumerate() { | |
| if cache_entry.line.contains(&pos) { | |
| return idx as isize; | |
| } | |
| } | |
| -1 | |
| } | |
| fn oldest_cache_entry_index(&self) -> usize { | |
| let mut oldest = 0; | |
| for idx in 1..self.line_cache.len() { | |
| if self.line_cache[idx].time_stamp < self.line_cache[oldest].time_stamp { | |
| oldest = idx; | |
| } | |
| } | |
| oldest | |
| } | |
| fn oldest_cache_entry_index_avoid(&self, avoid_idx: usize) -> usize { | |
| let mut oldest = if avoid_idx != 0 { 0 } else { 1 }; | |
| for idx in 0..self.line_cache.len() { | |
| if idx != avoid_idx | |
| && self.line_cache[idx].time_stamp < self.line_cache[oldest].time_stamp | |
| { | |
| oldest = idx; | |
| } | |
| } | |
| oldest | |
| } | |
| fn file_for_position(&self, pos: BytePos) -> Option<(Arc<SourceFile>, usize)> { | |
| if !self.source_map.files().is_empty() { | |
| let file_idx = self.source_map.lookup_source_file_idx(pos); | |
| let file = &self.source_map.files()[file_idx]; | |
| if file_contains(file, pos) { | |
| return Some((Arc::clone(file), file_idx)); | |
| } | |
| } | |
| None | |
| } | |
| } | |
| #[inline] | |
| fn file_contains(file: &SourceFile, pos: BytePos) -> bool { | |
| // `SourceMap::lookup_source_file_idx` and `SourceFile::contains` both consider the position | |
| // one past the end of a file to belong to it. Normally, that's what we want. But for the | |
| // purposes of converting a byte position to a line and column number, we can't come up with a | |
| // line and column number if the file is empty, because an empty file doesn't contain any | |
| // lines. So for our purposes, we don't consider empty files to contain any byte position. | |
| file.contains(pos) && !file.is_empty() | |
| } |