Skip to main content

microcad_lang_base/src_ref/
mod.rs

1// Copyright © 2024-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Source code references.
5//!
6//! All errors which occur while *parsing* or *evaluating* µcad code need to be reported and
7//! therefore need to address a place in the code where they did appear.
8//! A bunch of structs from this module provide this functionality:
9//!
10//! - [`SrcRef`] boxes a [`SrcRefInner`] which itself includes all necessary reference
11//!   information like *line*/*column* and a hash to identify the source file.
12//! - [`Refer`] encapsulates any syntax element and puts a [`SrcRef`] beside it.
13//! - [`SrcReferrer`] is a trait which provides unified access to the [`SrcRef`]
14//!   (e.g. implemented by [`Refer`]).
15
16mod line_col;
17mod refer;
18mod src_referrer;
19
20pub use line_col::*;
21pub use refer::*;
22pub use src_referrer::*;
23
24use derive_more::Deref;
25use miette::SourceSpan;
26
27/// Reference into a source file.
28///
29/// *Hint*: Source file is not part of `SrcRef` and must be provided from outside
30#[derive(Clone, Default, Deref)]
31pub struct SrcRef(pub Option<Box<SrcRefInner>>);
32
33impl SrcRef {
34    /// Create new `SrcRef`
35    /// - `range`: Position in file
36    /// - `line`: Line number within file
37    /// - `col`: Column number within file
38    pub fn new(
39        range: std::ops::Range<usize>,
40        line: usize,
41        col: usize,
42        source_file_hash: u64,
43    ) -> Self {
44        Self(Some(Box::new(SrcRefInner {
45            range,
46            at: LineCol { line, col },
47            source_file_hash,
48        })))
49    }
50
51    /// Return a span for the source reference as expected by miette
52    pub fn as_miette_span(&self) -> Option<SourceSpan> {
53        self.0
54            .as_ref()
55            .map(|s| SourceSpan::new(s.range.start.into(), s.range.len()))
56    }
57
58    /// Return a reference with a given line offset.
59    pub fn with_line_offset(self, offset: usize) -> Self {
60        Self(self.0.map(|inner| Box::new(inner.with_line_offset(offset))))
61    }
62}
63
64impl From<SrcRef> for SourceSpan {
65    fn from(value: SrcRef) -> Self {
66        value
67            .as_miette_span()
68            .unwrap_or(SourceSpan::new(0.into(), 0))
69    }
70}
71
72/// A reference into the source code
73#[derive(Clone, Default)]
74pub struct SrcRefInner {
75    /// Range in bytes
76    pub range: std::ops::Range<usize>,
77    /// Line and column
78    pub at: LineCol,
79    /// Hash of the source code file to map `SrcRef` -> `SourceFile`
80    pub source_file_hash: u64,
81}
82
83impl SrcRefInner {
84    /// Check if two source refs are overlapping.
85    pub fn is_overlapping(&self, other: &Self) -> bool {
86        self.source_file_hash != 0
87            && other.source_file_hash != 0
88            && (self.range.start < other.range.end)
89            && (other.range.start < self.range.end)
90    }
91
92    /// Return a reference with a given line offset.
93    pub fn with_line_offset(&self, line_offset: usize) -> Self {
94        let mut s = self.clone();
95        s.at.line += line_offset;
96        s
97    }
98}
99
100impl From<SrcRefInner> for SourceSpan {
101    fn from(value: SrcRefInner) -> Self {
102        SourceSpan::new(value.range.start.into(), value.range.len())
103    }
104}
105
106impl std::fmt::Display for SrcRef {
107    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
108        match &self.0 {
109            Some(s) => write!(f, "{}", s.at),
110            _ => write!(f, crate::invalid_no_ansi!(REF)),
111        }
112    }
113}
114
115impl std::fmt::Debug for SrcRef {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match &self.0 {
118            Some(s) => write!(
119                f,
120                "{} ({}..{}) in {:#x}",
121                s.at, s.range.start, s.range.end, s.source_file_hash
122            ),
123            _ => write!(f, crate::invalid!(REF)),
124        }
125    }
126}
127
128impl PartialEq for SrcRef {
129    fn eq(&self, _: &Self) -> bool {
130        true
131    }
132}
133
134impl PartialOrd for SrcRef {
135    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
136        Some(self.cmp(other))
137    }
138}
139
140impl Eq for SrcRef {}
141
142impl Ord for SrcRef {
143    fn cmp(&self, _: &Self) -> std::cmp::Ordering {
144        std::cmp::Ordering::Equal
145    }
146}
147
148impl SrcRef {
149    /// return length of `SrcRef`
150    pub fn len(&self) -> usize {
151        self.0.as_ref().map(|s| s.range.len()).unwrap_or(0)
152    }
153
154    /// return true if code base is empty
155    #[must_use]
156    pub fn is_empty(&self) -> bool {
157        self.len() == 0
158    }
159
160    /// return source file hash
161    /// - `0` if not `SrcRefInner` is none
162    /// - `u64` if `SrcRefInner` is some
163    ///
164    /// This is used to map `SrcRef` -> `SourceFile`
165    pub fn source_hash(&self) -> u64 {
166        self.0.as_ref().map(|s| s.source_file_hash).unwrap_or(0)
167    }
168
169    /// Return slice to code base.
170    pub fn source_slice<'a>(&self, src: &'a str) -> &'a str {
171        &src[self.0.as_ref().expect("SrcRef").range.to_owned()]
172    }
173
174    /// Merge two `SrcRef` into a single one.
175    ///
176    /// `SrcRef(None)` is returned if:
177    /// - ranges not in correct order (warning in log),
178    /// - references are not in the same file (warning in log),
179    /// - or `lhs` and `rhs` are both `None`.
180    pub fn merge(lhs: &impl SrcReferrer, rhs: &impl SrcReferrer) -> SrcRef {
181        match (lhs.src_ref(), rhs.src_ref()) {
182            (SrcRef(Some(lhs)), SrcRef(Some(rhs))) => {
183                if lhs.source_file_hash == rhs.source_file_hash {
184                    let source_file_hash = lhs.source_file_hash;
185
186                    if lhs.range == rhs.range {
187                        SrcRef(Some(lhs))
188                    } else if lhs.range.end > rhs.range.start || lhs.range.start > rhs.range.end {
189                        log::warn!(
190                            "ranges not in correct order: {lhs} vs {rhs} @ {source_file_hash}",
191                            lhs = lhs.at,
192                            rhs = rhs.at
193                        );
194                        SrcRef(None)
195                    } else {
196                        SrcRef(Some(Box::new(SrcRefInner {
197                            range: {
198                                // paranoia check
199                                assert!(lhs.range.end <= rhs.range.end);
200                                assert!(lhs.range.start <= rhs.range.start);
201
202                                lhs.range.start..rhs.range.end
203                            },
204                            at: lhs.at,
205                            source_file_hash,
206                        })))
207                    }
208                } else {
209                    log::warn!("references are not in the same file");
210                    SrcRef(None)
211                }
212            }
213            (SrcRef(Some(hs)), SrcRef(None)) | (SrcRef(None), SrcRef(Some(hs))) => SrcRef(Some(hs)),
214            _ => SrcRef(None),
215        }
216    }
217
218    /// Merge all given source references to one
219    ///
220    /// All  given source references must have the same hash otherwise panics!
221    pub fn merge_all<S: SrcReferrer>(referrers: impl Iterator<Item = S>) -> SrcRef {
222        let mut result = SrcRef(None);
223        for referrer in referrers {
224            if let Some(src_ref) = referrer.src_ref().0 {
225                if let SrcRef(Some(result)) = &mut result {
226                    if result.source_file_hash != src_ref.source_file_hash {
227                        panic!("can only merge source references of the same file");
228                    }
229                    if src_ref.range.start < result.range.start {
230                        result.range.start = src_ref.range.start;
231                        result.at = src_ref.at;
232                    }
233                    result.range.end = std::cmp::max(src_ref.range.end, result.range.end);
234                } else {
235                    result = SrcRef(Some(src_ref));
236                }
237            }
238        }
239        result
240    }
241
242    /// Return line and column in source code or `None` if not available.
243    pub fn at(&self) -> Option<LineCol> {
244        self.0.as_ref().map(|s| s.at.clone())
245    }
246    /// Returns `true` two source code references overlap.
247    ///
248    /// This means they must have the same non-zero source file hash and its ranges must overlap.
249    pub fn is_overlapping(&self, other: &Self) -> bool {
250        match (&self.0, &other.0) {
251            (Some(a), Some(b)) => a.is_overlapping(b),
252            _ => false,
253        }
254    }
255
256    /// Get the line of the start of the referenced source, if any
257    pub fn line(&self) -> Option<usize> {
258        self.0.as_ref().map(|inner| inner.at.line)
259    }
260
261    /// Get the column of the start of the referenced source, if any
262    pub fn col(&self) -> Option<usize> {
263        self.0.as_ref().map(|inner| inner.at.line)
264    }
265}
266
267#[test]
268fn merge_all() {
269    use std::ops::Range;
270    assert_eq!(
271        SrcRef::merge_all(
272            [
273                SrcRef::new(Range { start: 5, end: 8 }, 1, 6, 123),
274                SrcRef::new(Range { start: 8, end: 10 }, 2, 1, 123),
275                SrcRef::new(Range { start: 12, end: 16 }, 3, 1, 123),
276                SrcRef::new(Range { start: 0, end: 10 }, 1, 1, 123),
277            ]
278            .iter(),
279        ),
280        SrcRef::new(Range { start: 0, end: 16 }, 1, 1, 123),
281    );
282}
283
284#[test]
285fn test_src_ref() {
286    let input = "geo3d::Cube(size_x = 3.0, size_y = 3.0, size_z = 3.0);";
287
288    let cube = 7..11;
289    let size_y = 26..32;
290
291    let cube = SrcRef::new(cube, 1, 0, 0);
292    let size_y = SrcRef::new(size_y, 1, 0, 0);
293
294    assert_eq!(cube.source_slice(input), "Cube");
295    assert_eq!(size_y.source_slice(input), "size_y");
296}