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::{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}