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