Skip to main content

rustc_span/
caching_source_map_view.rs

1use std::ops::Range;
2use std::sync::Arc;
3
4use crate::source_map::SourceMap;
5use crate::{BytePos, Pos, RelativeBytePos, SourceFile, SpanData};
6
7/// A `SourceMap` wrapper that caches info about a single recent code position. This gives a good
8/// speedup when hashing spans, because often multiple spans within a single line are hashed in
9/// succession, and this avoids expensive `SourceMap` lookups each time the cache is hit. We used
10/// to cache multiple code positions, but caching a single position ended up being simpler and
11/// faster.
12pub struct CachingSourceMapView<'sm> {
13    source_map: &'sm SourceMap,
14    file: Arc<SourceFile>,
15    // The line's byte position range in the `SourceMap`. This range will fail to contain a valid
16    // position in certain edge cases. Spans often start/end one past something, and when that
17    // something is the last character of a file (this can happen when a file doesn't end in a
18    // newline, for example), we'd still like for the position to be considered within the last
19    // line. However, it isn't according to the exclusive upper bound of this range. We cannot
20    // change the upper bound to be inclusive, because for most lines, the upper bound is the same
21    // as the lower bound of the next line, so there would be an ambiguity.
22    //
23    // Since the containment aspect of this range is only used to see whether or not the cache
24    // entry contains a position, the only ramification of the above is that we will get cache
25    // misses for these rare positions. A line lookup for the position via `SourceMap::lookup_line`
26    // after a cache miss will produce the last line number, as desired.
27    line_bounds: Range<BytePos>,
28    line_number: usize,
29}
30
31impl<'sm> CachingSourceMapView<'sm> {
32    pub fn new(source_map: &'sm SourceMap) -> CachingSourceMapView<'sm> {
33        let files = source_map.files();
34        let first_file = Arc::clone(&files[0]);
35        CachingSourceMapView {
36            source_map,
37            file: first_file,
38            line_bounds: BytePos(0)..BytePos(0),
39            line_number: 0,
40        }
41    }
42
43    #[inline]
44    fn pos_to_line(&self, pos: BytePos) -> (Range<BytePos>, usize) {
45        let pos = self.file.relative_position(pos);
46        let line_index = self.file.lookup_line(pos).unwrap();
47        let line_bounds = self.file.line_bounds(line_index);
48        let line_number = line_index + 1;
49        (line_bounds, line_number)
50    }
51
52    #[inline]
53    fn update(&mut self, new_file: Option<Arc<SourceFile>>, pos: BytePos) {
54        if let Some(file) = new_file {
55            self.file = file;
56        }
57        (self.line_bounds, self.line_number) = self.pos_to_line(pos);
58    }
59
60    pub fn byte_pos_to_line_and_col(
61        &mut self,
62        pos: BytePos,
63    ) -> Option<(Arc<SourceFile>, usize, RelativeBytePos)> {
64        if self.line_bounds.contains(&pos) {
65            // Cache hit: do nothing.
66        } else {
67            // Cache miss. If the entry doesn't point to the correct file, get the new file and
68            // index.
69            let new_file = if !file_contains(&self.file, pos) {
70                Some(self.file_for_position(pos)?)
71            } else {
72                None
73            };
74            self.update(new_file, pos);
75        };
76
77        let col = RelativeBytePos(pos.to_u32() - self.line_bounds.start.to_u32());
78        Some((Arc::clone(&self.file), self.line_number, col))
79    }
80
81    pub fn span_data_to_lines_and_cols(
82        &mut self,
83        span_data: &SpanData,
84    ) -> Option<(&SourceFile, usize, BytePos, usize, BytePos)> {
85        let lo_hit = self.line_bounds.contains(&span_data.lo);
86        let hi_hit = self.line_bounds.contains(&span_data.hi);
87        if lo_hit && hi_hit {
88            // span_data.lo and span_data.hi are cached (i.e. both in the same line).
89            return Some((
90                &self.file,
91                self.line_number,
92                span_data.lo - self.line_bounds.start,
93                self.line_number,
94                span_data.hi - self.line_bounds.start,
95            ));
96        }
97
98        // If the cached file is wrong, update it. Return early if the span lo and hi are in
99        // different files.
100        let new_file = if !file_contains(&self.file, span_data.lo) {
101            let new_file = self.file_for_position(span_data.lo)?;
102            if !file_contains(&new_file, span_data.hi) {
103                return None;
104            }
105            Some(new_file)
106        } else {
107            if !file_contains(&self.file, span_data.hi) {
108                return None;
109            }
110            None
111        };
112
113        // If we reach here, lo and hi are in the same file.
114
115        if !lo_hit {
116            // We cache the lo information.
117            self.update(new_file, span_data.lo);
118        }
119        let lo_line_bounds = &self.line_bounds;
120        let lo_line_number = self.line_number.clone();
121
122        let (hi_line_bounds, hi_line_number) = if !self.line_bounds.contains(&span_data.hi) {
123            // hi and lo are in different lines. We compute but don't cache the hi information.
124            self.pos_to_line(span_data.hi)
125        } else {
126            // hi and lo are in the same line.
127            (self.line_bounds.clone(), self.line_number)
128        };
129
130        // Span lo and hi may equal line end when last line doesn't
131        // end in newline, hence the inclusive upper bounds below.
132        if !(span_data.lo >= lo_line_bounds.start) {
    ::core::panicking::panic("assertion failed: span_data.lo >= lo_line_bounds.start")
};assert!(span_data.lo >= lo_line_bounds.start);
133        if !(span_data.lo <= lo_line_bounds.end) {
    ::core::panicking::panic("assertion failed: span_data.lo <= lo_line_bounds.end")
};assert!(span_data.lo <= lo_line_bounds.end);
134        if !(span_data.hi >= hi_line_bounds.start) {
    ::core::panicking::panic("assertion failed: span_data.hi >= hi_line_bounds.start")
};assert!(span_data.hi >= hi_line_bounds.start);
135        if !(span_data.hi <= hi_line_bounds.end) {
    ::core::panicking::panic("assertion failed: span_data.hi <= hi_line_bounds.end")
};assert!(span_data.hi <= hi_line_bounds.end);
136        if !self.file.contains(span_data.lo) {
    ::core::panicking::panic("assertion failed: self.file.contains(span_data.lo)")
};assert!(self.file.contains(span_data.lo));
137        if !self.file.contains(span_data.hi) {
    ::core::panicking::panic("assertion failed: self.file.contains(span_data.hi)")
};assert!(self.file.contains(span_data.hi));
138
139        Some((
140            &self.file,
141            lo_line_number,
142            span_data.lo - lo_line_bounds.start,
143            hi_line_number,
144            span_data.hi - hi_line_bounds.start,
145        ))
146    }
147
148    fn file_for_position(&self, pos: BytePos) -> Option<Arc<SourceFile>> {
149        if !self.source_map.files().is_empty() {
150            let file_idx = self.source_map.lookup_source_file_idx(pos);
151            let file = &self.source_map.files()[file_idx];
152
153            if file_contains(file, pos) {
154                return Some(Arc::clone(file));
155            }
156        }
157
158        None
159    }
160}
161
162#[inline]
163fn file_contains(file: &SourceFile, pos: BytePos) -> bool {
164    // `SourceMap::lookup_source_file_idx` and `SourceFile::contains` both consider the position
165    // one past the end of a file to belong to it. Normally, that's what we want. But for the
166    // purposes of converting a byte position to a line and column number, we can't come up with a
167    // line and column number if the file is empty, because an empty file doesn't contain any
168    // lines. So for our purposes, we don't consider empty files to contain any byte position.
169    file.contains(pos) && !file.is_empty()
170}