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    /// Returns the source contents that is spanned by `span`.
92    pub fn contents_of_span(&self, span: Span) -> Option<String> {
93        let source_file1 = self.find_source_file(span.lo)?;
94        let source_file2 = self.find_source_file(span.hi)?;
95        assert_eq!(source_file1.absolute_start, source_file2.absolute_start);
96        Some(source_file1.contents_of_span(span).to_string())
97    }
98}
99
100impl SourceMapInner {
101    /// Attempt reserving address space for `size` number of bytes.
102    fn try_allocate_address_space(&mut self, size: u32) -> Option<u32> {
103        let current = self.used_address_space;
104        // By adding one, we can distinguish between files, even when they are empty.
105        self.used_address_space = current.checked_add(size)?.checked_add(1)?;
106        Some(current)
107    }
108}
109
110/// A file name.
111///
112/// This is either a wrapper around `PathBuf`,
113/// or a custom string description.
114#[derive(Clone)]
115pub enum FileName {
116    /// A real file.
117    Real(PathBuf),
118    /// Any sort of description for a source.
119    Custom(String),
120}
121
122impl fmt::Display for FileName {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::Real(x) if is_color() => x.display().fmt(f),
126            Self::Real(_) => Ok(()),
127            Self::Custom(x) => f.write_str(x),
128        }
129    }
130}
131
132/// Is the env var `NOCOLOR` not enabled?
133pub fn is_color() -> bool {
134    std::env::var("NOCOLOR").unwrap_or_default().trim().is_empty()
135}
136
137/// A single source in the [`SourceMap`].
138pub struct SourceFile {
139    /// The name of the file that the source came from.
140    pub name: FileName,
141    /// The complete source code.
142    pub src: String,
143    /// The start position of this source in the `SourceMap`.
144    pub absolute_start: u32,
145    /// The end position of this source in the `SourceMap`.
146    pub absolute_end: u32,
147}
148
149impl SourceFile {
150    /// Creates a new `SourceFile`.
151    fn new(name: FileName, src: String, absolute_start: u32) -> Self {
152        let absolute_end = absolute_start + src.len() as u32;
153        Self { name, src, absolute_start, absolute_end }
154    }
155
156    /// Converts an absolute offset to a file-relative offset
157    pub fn relative_offset(&self, absolute_offset: u32) -> u32 {
158        assert!(self.absolute_start <= absolute_offset);
159        assert!(absolute_offset <= self.absolute_end);
160        absolute_offset - self.absolute_start
161    }
162
163    /// Returns contents of a `span` assumed to be within the given file.
164    pub fn contents_of_span(&self, span: Span) -> &str {
165        let start = self.relative_offset(span.lo);
166        let end = self.relative_offset(span.hi);
167        &self.src[start as usize..end as usize]
168    }
169
170    pub fn line_col(&self, absolute_offset: u32) -> (u32, u32) {
171        let relative_offset = self.relative_offset(absolute_offset);
172        let mut current_offset = 0u32;
173
174        for (i, line) in self.src.split('\n').enumerate() {
175            let end_of_line = current_offset + line.len() as u32;
176            if relative_offset <= end_of_line {
177                let chars = self.src[current_offset as usize..relative_offset as usize].chars().count();
178                return (i as u32, chars as u32);
179            }
180            current_offset = end_of_line + 1;
181        }
182
183        panic!("Can't happen.");
184    }
185
186    pub fn line_contents(&self, span: Span) -> LineContents<'_> {
187        let start = self.relative_offset(span.lo) as usize;
188        let end = self.relative_offset(span.hi) as usize;
189
190        let line_start = self.src[..=start].rfind('\n').map(|i| i + 1).unwrap_or(0);
191        let line_end = self.src[end..].find('\n').map(|x| x + end).unwrap_or(self.src.len());
192
193        LineContents {
194            line: self.src[..line_start].lines().count(),
195            contents: &self.src[line_start..line_end],
196            start: start.saturating_sub(line_start),
197            end: end.saturating_sub(line_start),
198        }
199    }
200}
201
202pub struct LineContents<'a> {
203    pub contents: &'a str,
204    pub line: usize,
205    pub start: usize,
206    pub end: usize,
207}
208
209impl fmt::Display for LineContents<'_> {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        const INDENT: &str = "    ";
212
213        let mut current_underline = String::new();
214        let mut line = self.line;
215        let mut line_beginning = true;
216        let mut underline_started = false;
217
218        writeln!(f, "{INDENT} |")?;
219
220        for (i, c) in self.contents.chars().enumerate() {
221            if line_beginning {
222                write!(
223                    f,
224                    "{line:width$} | ",
225                    // Report lines starting from 1.
226                    line = line + 1,
227                    width = INDENT.len()
228                )?;
229            }
230            if c == '\n' {
231                writeln!(f)?;
232                // Output the underline, without trailing whitespace.
233                let underline = current_underline.trim_end();
234                if !underline.is_empty() {
235                    writeln!(f, "{INDENT} | {underline}")?;
236                }
237                underline_started = false;
238                current_underline.clear();
239                line += 1;
240                line_beginning = true;
241            } else {
242                line_beginning = false;
243                if c != '\r' {
244                    write!(f, "{c}")?;
245                    if self.start <= i && i < self.end && (underline_started || !c.is_whitespace()) {
246                        underline_started = true;
247                        current_underline.push('^');
248                    } else {
249                        current_underline.push(' ');
250                    }
251                }
252            }
253        }
254
255        // If the text didn't end in a newline, we may still
256        // need to output an underline.
257        let underline = current_underline.trim_end();
258        if !underline.is_empty() {
259            writeln!(f, "\n{INDENT} | {underline}")?;
260        }
261
262        Ok(())
263    }
264}
265
266/// File / Line / Column information on a `BytePos`.
267pub struct LineCol {
268    /// Information on the original source.
269    pub source_file: Rc<SourceFile>,
270    /// The line number.
271    pub line: u32,
272    /// The column offset into the line.
273    pub col: u32,
274}