microcad_lang/src_ref/
mod.rs

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