blob: 5c8b1400f36bca58d67af5bd023dc5b0f7796428 [file] [log] [blame]
use std::ops::Range;
use std::sync::Arc;
use crate::source_map::SourceMap;
use crate::{BytePos, Pos, RelativeBytePos, SourceFile, SpanData};
/// A `SourceMap` wrapper that caches info about a single recent code position. This gives a good
/// speedup when hashing spans, because often multiple spans within a single line are hashed in
/// succession, and this avoids expensive `SourceMap` lookups each time the cache is hit. We used
/// to cache multiple code positions, but caching a single position ended up being simpler and
/// faster.
pub struct CachingSourceMapView<'sm> {
source_map: &'sm SourceMap,
file: Arc<SourceFile>,
// 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_bounds: Range<BytePos>,
line_number: 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]);
CachingSourceMapView {
source_map,
file: first_file,
line_bounds: BytePos(0)..BytePos(0),
line_number: 0,
}
}
#[inline]
fn pos_to_line(&self, pos: BytePos) -> (Range<BytePos>, usize) {
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);
let line_number = line_index + 1;
(line_bounds, line_number)
}
#[inline]
fn update(&mut self, new_file: Option<Arc<SourceFile>>, pos: BytePos) {
if let Some(file) = new_file {
self.file = file;
}
(self.line_bounds, self.line_number) = self.pos_to_line(pos);
}
pub fn byte_pos_to_line_and_col(
&mut self,
pos: BytePos,
) -> Option<(Arc<SourceFile>, usize, RelativeBytePos)> {
if self.line_bounds.contains(&pos) {
// Cache hit: do nothing.
} else {
// Cache miss. If the entry doesn't point to the correct file, get the new file and
// index.
let new_file = if !file_contains(&self.file, pos) {
Some(self.file_for_position(pos)?)
} else {
None
};
self.update(new_file, pos);
};
let col = RelativeBytePos(pos.to_u32() - self.line_bounds.start.to_u32());
Some((Arc::clone(&self.file), self.line_number, col))
}
pub fn span_data_to_lines_and_cols(
&mut self,
span_data: &SpanData,
) -> Option<(&SourceFile, usize, BytePos, usize, BytePos)> {
let lo_hit = self.line_bounds.contains(&span_data.lo);
let hi_hit = self.line_bounds.contains(&span_data.hi);
if lo_hit && hi_hit {
// span_data.lo and span_data.hi are cached (i.e. both in the same line).
return Some((
&self.file,
self.line_number,
span_data.lo - self.line_bounds.start,
self.line_number,
span_data.hi - self.line_bounds.start,
));
}
// If the cached file is wrong, update it. Return early if the span lo and hi are in
// different files.
let new_file = if !file_contains(&self.file, span_data.lo) {
let new_file = self.file_for_position(span_data.lo)?;
if !file_contains(&new_file, span_data.hi) {
return None;
}
Some(new_file)
} else {
if !file_contains(&self.file, span_data.hi) {
return None;
}
None
};
// If we reach here, lo and hi are in the same file.
if !lo_hit {
// We cache the lo information.
self.update(new_file, span_data.lo);
}
let lo_line_bounds = &self.line_bounds;
let lo_line_number = self.line_number.clone();
let (hi_line_bounds, hi_line_number) = if !self.line_bounds.contains(&span_data.hi) {
// hi and lo are in different lines. We compute but don't cache the hi information.
self.pos_to_line(span_data.hi)
} else {
// hi and lo are in the same line.
(self.line_bounds.clone(), self.line_number)
};
// 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_bounds.start);
assert!(span_data.lo <= lo_line_bounds.end);
assert!(span_data.hi >= hi_line_bounds.start);
assert!(span_data.hi <= hi_line_bounds.end);
assert!(self.file.contains(span_data.lo));
assert!(self.file.contains(span_data.hi));
Some((
&self.file,
lo_line_number,
span_data.lo - lo_line_bounds.start,
hi_line_number,
span_data.hi,
))
}
fn file_for_position(&self, pos: BytePos) -> Option<Arc<SourceFile>> {
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));
}
}
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()
}