leo_span/
source_map.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! The source map provides an address space for positions in spans
18//! that is global across the source files that are compiled together.
19//! The source files are organized in a sequence,
20//! with the positions of each source following the ones of the previous source
21//! in the address space of positions
22//! (except for the first source, which starts at the beginning of the address space).
23//! This way, any place in any source is identified by a single position
24//! within the address space covered by the sequence of sources;
25//! the source file is determined from the position.
26
27use crate::span::Span;
28
29use std::{
30    cell::RefCell,
31    fmt,
32    fs,
33    io,
34    path::{Path, PathBuf},
35    rc::Rc,
36};
37
38/// The source map containing all recorded sources,
39/// methods to register new ones,
40/// and methods to query about spans in relation to recorded sources.
41#[derive(Default)]
42pub struct SourceMap {
43    /// The actual source map data.
44    inner: RefCell<SourceMapInner>,
45}
46
47/// Actual data of the source map.
48/// We use this setup for purposes of interior mutability.
49#[derive(Default)]
50struct SourceMapInner {
51    /// The address space below this value is currently used by the files in the source map.
52    used_address_space: u32,
53
54    /// All the source files recorded thus far.
55    ///
56    /// The list is append-only with mappings from the start byte position
57    /// for fast lookup from a `Span` to its `SourceFile`.
58    source_files: Vec<Rc<SourceFile>>,
59}
60
61impl SourceMap {
62    /// Loads the given `path` and returns a `SourceFile` for it.
63    pub fn load_file(&self, path: &Path) -> io::Result<Rc<SourceFile>> {
64        Ok(self.new_source(&fs::read_to_string(path)?, FileName::Real(path.to_owned())))
65    }
66
67    /// Registers `source` under the given file `name`, returning a `SourceFile` back.
68    pub fn new_source(&self, source: &str, name: FileName) -> Rc<SourceFile> {
69        let len = u32::try_from(source.len()).unwrap();
70        let mut inner = self.inner.borrow_mut();
71        let start_pos = inner.try_allocate_address_space(len).unwrap();
72        let source_file = Rc::new(SourceFile::new(name, source.to_owned(), start_pos));
73        inner.source_files.push(source_file.clone());
74        source_file
75    }
76
77    /// Find the index for the source file containing `pos`.
78    fn find_source_file_index(&self, pos: u32) -> Option<usize> {
79        self.inner
80            .borrow()
81            .source_files
82            .binary_search_by_key(&pos, |file| file.absolute_start)
83            .map_or_else(|p| p.checked_sub(1), Some)
84    }
85
86    /// Find the source file containing `pos`.
87    pub fn find_source_file(&self, pos: u32) -> Option<Rc<SourceFile>> {
88        Some(self.inner.borrow().source_files[self.find_source_file_index(pos)?].clone())
89    }
90
91    pub fn source_file_by_filename(&self, filename: &FileName) -> Option<Rc<SourceFile>> {
92        // TODO: This linear search could be improved to a hash lookup with some adjustment.
93        self.inner.borrow().source_files.iter().find(|source_file| &source_file.name == filename).cloned()
94    }
95
96    /// Returns the source contents that is spanned by `span`.
97    pub fn contents_of_span(&self, span: Span) -> Option<String> {
98        let source_file1 = self.find_source_file(span.lo)?;
99        let source_file2 = self.find_source_file(span.hi)?;
100        assert_eq!(source_file1.absolute_start, source_file2.absolute_start);
101        Some(source_file1.contents_of_span(span).to_string())
102    }
103}
104
105impl SourceMapInner {
106    /// Attempt reserving address space for `size` number of bytes.
107    fn try_allocate_address_space(&mut self, size: u32) -> Option<u32> {
108        let current = self.used_address_space;
109        // By adding one, we can distinguish between files, even when they are empty.
110        self.used_address_space = current.checked_add(size)?.checked_add(1)?;
111        Some(current)
112    }
113}
114
115/// A file name.
116///
117/// This is either a wrapper around `PathBuf`,
118/// or a custom string description.
119#[derive(Clone, Eq, PartialEq, Hash)]
120pub enum FileName {
121    /// A real file.
122    Real(PathBuf),
123    /// Any sort of description for a source.
124    Custom(String),
125}
126
127impl fmt::Display for FileName {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            Self::Real(x) if is_color() => x.display().fmt(f),
131            Self::Real(_) => Ok(()),
132            Self::Custom(x) => f.write_str(x),
133        }
134    }
135}
136
137/// Is the env var `NOCOLOR` not enabled?
138pub fn is_color() -> bool {
139    std::env::var("NOCOLOR").unwrap_or_default().trim().is_empty()
140}
141
142/// A single source in the [`SourceMap`].
143pub struct SourceFile {
144    /// The name of the file that the source came from.
145    pub name: FileName,
146    /// The complete source code.
147    pub src: String,
148    /// The start position of this source in the `SourceMap`.
149    pub absolute_start: u32,
150    /// The end position of this source in the `SourceMap`.
151    pub absolute_end: u32,
152}
153
154impl SourceFile {
155    /// Creates a new `SourceFile`.
156    fn new(name: FileName, src: String, absolute_start: u32) -> Self {
157        let absolute_end = absolute_start + src.len() as u32;
158        Self { name, src, absolute_start, absolute_end }
159    }
160
161    /// Converts an absolute offset to a file-relative offset
162    pub fn relative_offset(&self, absolute_offset: u32) -> u32 {
163        assert!(self.absolute_start <= absolute_offset);
164        assert!(absolute_offset <= self.absolute_end);
165        absolute_offset - self.absolute_start
166    }
167
168    /// Returns contents of a `span` assumed to be within the given file.
169    pub fn contents_of_span(&self, span: Span) -> &str {
170        let start = self.relative_offset(span.lo);
171        let end = self.relative_offset(span.hi);
172        &self.src[start as usize..end as usize]
173    }
174
175    pub fn line_col(&self, absolute_offset: u32) -> (u32, u32) {
176        let relative_offset = self.relative_offset(absolute_offset);
177        let mut current_offset = 0u32;
178
179        for (i, line) in self.src.split('\n').enumerate() {
180            let end_of_line = current_offset + line.len() as u32;
181            if relative_offset <= end_of_line {
182                let chars = self.src[current_offset as usize..relative_offset as usize].chars().count();
183                return (i as u32, chars as u32);
184            }
185            current_offset = end_of_line + 1;
186        }
187
188        panic!("Can't happen.");
189    }
190
191    pub fn line_contents(&self, span: Span) -> LineContents<'_> {
192        let start = self.relative_offset(span.lo) as usize;
193        let end = self.relative_offset(span.hi) as usize;
194
195        let line_start = self.src[..=start].rfind('\n').map(|i| i + 1).unwrap_or(0);
196        let line_end = self.src[end..].find('\n').map(|x| x + end).unwrap_or(self.src.len());
197
198        LineContents {
199            line: self.src[..line_start].lines().count(),
200            contents: &self.src[line_start..line_end],
201            start: start.saturating_sub(line_start),
202            end: end.saturating_sub(line_start),
203        }
204    }
205}
206
207pub struct LineContents<'a> {
208    pub contents: &'a str,
209    pub line: usize,
210    pub start: usize,
211    pub end: usize,
212}
213
214impl fmt::Display for LineContents<'_> {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        const INDENT: &str = "    ";
217
218        let mut current_underline = String::new();
219        let mut line = self.line;
220        let mut line_beginning = true;
221        let mut underline_started = false;
222
223        writeln!(f, "{INDENT} |")?;
224
225        for (i, c) in self.contents.chars().enumerate() {
226            if line_beginning {
227                write!(
228                    f,
229                    "{line:width$} | ",
230                    // Report lines starting from 1.
231                    line = line + 1,
232                    width = INDENT.len()
233                )?;
234            }
235            if c == '\n' {
236                writeln!(f)?;
237                // Output the underline, without trailing whitespace.
238                let underline = current_underline.trim_end();
239                if !underline.is_empty() {
240                    writeln!(f, "{INDENT} | {underline}")?;
241                }
242                underline_started = false;
243                current_underline.clear();
244                line += 1;
245                line_beginning = true;
246            } else {
247                line_beginning = false;
248                if c != '\r' {
249                    write!(f, "{c}")?;
250                    if self.start <= i && i < self.end && (underline_started || !c.is_whitespace()) {
251                        underline_started = true;
252                        current_underline.push('^');
253                    } else {
254                        current_underline.push(' ');
255                    }
256                }
257            }
258        }
259
260        // If the text didn't end in a newline, we may still
261        // need to output an underline.
262        let underline = current_underline.trim_end();
263        if !underline.is_empty() {
264            writeln!(f, "\n{INDENT} | {underline}")?;
265        }
266
267        Ok(())
268    }
269}
270
271/// File / Line / Column information on a `BytePos`.
272pub struct LineCol {
273    /// Information on the original source.
274    pub source_file: Rc<SourceFile>,
275    /// The line number.
276    pub line: u32,
277    /// The column offset into the line.
278    pub col: u32,
279}