shrimple_parser/
loc.rs

1extern crate alloc;
2
3use {
4    crate::{nonzero, utils::PathLike},
5    alloc::borrow::Cow,
6    core::{
7        char::REPLACEMENT_CHARACTER,
8        fmt::{Display, Formatter, Write},
9        num::NonZero,
10    },
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14/// Location of the error. Useful for error reporting, and used by [`crate::FullParsingError`]
15pub struct Location {
16    /// Source code line of the location.
17    pub line: NonZero<u32>,
18    /// Source code column of the location.
19    pub col: u32,
20}
21
22impl Default for Location {
23    fn default() -> Self {
24        Self {
25            line: nonzero!(1),
26            col: 0,
27        }
28    }
29}
30
31impl Display for Location {
32    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
33        write!(f, "{}:{}", self.line, self.col)
34    }
35}
36
37impl Location {
38    /// Returns the [`Location`] that's calculated with `self` as the base & `rhs` as the offset
39    /// from the base.
40    ///
41    /// # Panics
42    /// Panics if the line or column overflow a `u32`
43    #[must_use]
44    pub const fn offset(self, rhs: Self) -> Self {
45        if rhs.line.get() == 1 {
46            Self {
47                line: self.line,
48                col: self.col - rhs.col,
49            }
50        } else {
51            Self {
52                line: NonZero::new(self.line.get() - rhs.line.get() + 1).expect("no overflow"),
53                col: rhs.col,
54            }
55        }
56    }
57
58    /// Turn a [`Location`] into a [`FullLocation`] by providing the file path.
59    pub fn with_path<'path>(self, path: impl PathLike<'path>) -> FullLocation<'path> {
60        FullLocation {
61            path: path.into_path_bytes(),
62            loc: self,
63        }
64    }
65
66    /// Locates address `ptr` in `src` and returns its source code location, or None if `ptr` is
67    /// outside of the memory range of `src`.
68    pub fn find(ptr: *const u8, src: &str) -> Option<Self> {
69        let progress =
70            usize::checked_sub(ptr as _, src.as_ptr() as _).filter(|x| *x <= src.len())?;
71
72        Some(
73            src.bytes()
74                .take(progress)
75                .fold(Self::default(), |loc, b| match b {
76                    b'\n' => Self {
77                        line: loc.line.saturating_add(1),
78                        col: 0,
79                    },
80                    _ => Self {
81                        col: loc.col.saturating_add(1),
82                        ..loc
83                    },
84                }),
85        )
86    }
87
88    /// Same as [`find`](Self::find), except for the `None` case:
89    /// - If `ptr` is before `src`, the returned location points to the beginning of `src`.
90    /// - If `ptr` is after `src`, the returned location points to the end of `src`.
91    ///
92    /// This function is used by [`crate::ParsingError::with_src_loc`]
93    pub fn find_saturating(ptr: *const u8, src: &str) -> Self {
94        let progress = usize::saturating_sub(ptr as _, src.as_ptr() as _);
95
96        let res = src
97            .bytes()
98            .take(progress)
99            .fold(Self::default(), |loc, b| match b {
100                b'\n' => Self {
101                    line: loc.line.saturating_add(1),
102                    col: 0,
103                },
104                _ => Self {
105                    col: loc.col.saturating_add(1),
106                    ..loc
107                },
108            });
109        res
110    }
111
112    /// Same as [`find`](Self::find), but searches in multiple "files".
113    ///
114    /// A file, per definition of this function, is a key `K` that identifies it,
115    /// and a memory range that is its content.
116    /// The function returns the key of the file where `ptr` is contained, or `None` if no files
117    /// matched.
118    /// ```rust
119    /// # fn main() {
120    /// use std::collections::HashMap;
121    /// use shrimple_parser::{utils::Location, nonzero, tuple::copied};
122    ///
123    /// let file2 = "          \n\nfn main() { panic!() }";
124    /// let sources = HashMap::from([
125    ///     ("file1.rs", r#"fn main() { println!("Hiiiii!!!!! :3") }"#),
126    ///     ("file2.rs", file2),
127    /// ]);
128    /// let no_ws = file2.trim();
129    /// assert_eq!(
130    ///     Location::find_in_multiple(no_ws.as_ptr(), sources.iter().map(copied)),
131    ///     Some(("file2.rs", Location { line: nonzero!(3), col: 0 })),
132    /// )
133    /// # }
134    /// ```
135    /// Also see [`tuple::copied`], [`nonzero`]
136    pub fn find_in_multiple<K>(
137        ptr: *const u8,
138        files: impl IntoIterator<Item = (K, impl AsRef<str>)>,
139    ) -> Option<(K, Self)> {
140        files
141            .into_iter()
142            .find_map(|(k, src)| Some((k, Self::find(ptr, src.as_ref())?)))
143    }
144}
145
146/// Like [`Location`], but also stores the path to the file.
147#[derive(Debug, Clone, PartialEq, Eq, Hash)]
148pub struct FullLocation<'path> {
149    /// The path to the file associated with the location.
150    pub path: Cow<'path, [u8]>,
151    /// The line & column numbers of the location.
152    pub loc: Location,
153}
154
155impl Display for FullLocation<'_> {
156    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
157        for chunk in self.path.utf8_chunks() {
158            f.write_str(chunk.valid())?;
159            if !chunk.invalid().is_empty() {
160                f.write_char(REPLACEMENT_CHARACTER)?;
161            }
162        }
163        write!(f, ":{}", self.loc)
164    }
165}
166
167impl FullLocation<'_> {
168    /// Unbind the location from the lifetimes by allocating the path if it hasn't been already.
169    pub fn own(self) -> FullLocation<'static> {
170        FullLocation {
171            path: self.path.into_owned().into(),
172            loc: self.loc,
173        }
174    }
175}