Skip to main content

leo_span/
source_map.rs

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