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;
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/// A reference into the source code
52#[derive(Clone, Default)]
53pub struct SrcRefInner {
54    /// Range in bytes
55    pub range: std::ops::Range<usize>,
56    /// Line and column
57    pub at: LineCol,
58    /// Hash of the source code file to map `SrcRef` -> `SourceFile`
59    pub source_file_hash: u64,
60}
61
62impl std::fmt::Display for SrcRef {
63    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64        match &self.0 {
65            Some(s) => write!(f, "{}", s.at),
66            _ => write!(f, crate::invalid_no_ansi!(REF)),
67        }
68    }
69}
70
71impl std::fmt::Debug for SrcRef {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match &self.0 {
74            Some(s) => write!(
75                f,
76                "{} ({}..{}) in {:#x}",
77                s.at, s.range.start, s.range.end, s.source_file_hash
78            ),
79            _ => write!(f, crate::invalid!(REF)),
80        }
81    }
82}
83
84impl PartialEq for SrcRef {
85    fn eq(&self, _: &Self) -> bool {
86        true
87    }
88}
89
90impl PartialOrd for SrcRef {
91    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
92        Some(self.cmp(other))
93    }
94}
95
96impl Eq for SrcRef {}
97
98impl Ord for SrcRef {
99    fn cmp(&self, _: &Self) -> std::cmp::Ordering {
100        std::cmp::Ordering::Equal
101    }
102}
103
104impl SrcRef {
105    /// return length of `SrcRef`
106    pub fn len(&self) -> usize {
107        self.0.as_ref().map(|s| s.range.len()).unwrap_or(0)
108    }
109
110    /// return true if code base is empty
111    #[must_use]
112    pub fn is_empty(&self) -> bool {
113        self.len() == 0
114    }
115
116    /// return source file hash
117    /// - `0` if not `SrcRefInner` is none
118    /// - `u64` if `SrcRefInner` is some
119    ///
120    /// This is used to map `SrcRef` -> `SourceFile`
121    pub fn source_hash(&self) -> u64 {
122        self.0.as_ref().map(|s| s.source_file_hash).unwrap_or(0)
123    }
124
125    /// Return slice to code base.
126    pub fn source_slice<'a>(&self, src: &'a str) -> &'a str {
127        &src[self.0.as_ref().expect("SrcRef").range.to_owned()]
128    }
129
130    /// Merge two `SrcRef` into a single one.
131    ///
132    /// `SrcRef(None)` is returned if:
133    /// - ranges not in correct order (warning in log),
134    /// - references are not in the same file (warning in log),
135    /// - or `lhs` and `rhs` are both `None`.
136    pub fn merge(lhs: &impl SrcReferrer, rhs: &impl SrcReferrer) -> SrcRef {
137        match (lhs.src_ref(), rhs.src_ref()) {
138            (SrcRef(Some(lhs)), SrcRef(Some(rhs))) => {
139                if lhs.source_file_hash == rhs.source_file_hash {
140                    let source_file_hash = lhs.source_file_hash;
141
142                    if lhs.range.end > rhs.range.start || lhs.range.start > rhs.range.end {
143                        log::warn!("ranges not in correct order");
144                        SrcRef(None)
145                    } else {
146                        SrcRef(Some(Box::new(SrcRefInner {
147                            range: {
148                                // paranoia check
149                                assert!(lhs.range.end <= rhs.range.end);
150                                assert!(lhs.range.start <= rhs.range.start);
151
152                                lhs.range.start..rhs.range.end
153                            },
154                            at: lhs.at,
155                            source_file_hash,
156                        })))
157                    }
158                } else {
159                    log::warn!("references are not in the same file");
160                    SrcRef(None)
161                }
162            }
163            (SrcRef(Some(hs)), SrcRef(None)) | (SrcRef(None), SrcRef(Some(hs))) => SrcRef(Some(hs)),
164            _ => SrcRef(None),
165        }
166    }
167
168    /// Merge all given source references to one
169    ///
170    /// All  given source references must have the same hash otherwise panics!
171    pub fn merge_all<S: SrcReferrer>(referrers: impl Iterator<Item = S>) -> SrcRef {
172        let mut result = SrcRef(None);
173        for referrer in referrers {
174            if let Some(src_ref) = referrer.src_ref().0 {
175                if let SrcRef(Some(result)) = &mut result {
176                    if result.source_file_hash != src_ref.source_file_hash {
177                        panic!("can only merge source references of the same file");
178                    }
179                    if src_ref.range.start < result.range.start {
180                        result.range.start = src_ref.range.start;
181                        result.at = src_ref.at;
182                    }
183                    result.range.end = std::cmp::max(src_ref.range.end, result.range.end);
184                } else {
185                    result = SrcRef(Some(src_ref));
186                }
187            }
188        }
189        result
190    }
191
192    /// Return line and column in source code or `None` if not available.
193    pub fn at(&self) -> Option<LineCol> {
194        self.0.as_ref().map(|s| s.at.clone())
195    }
196}
197
198#[test]
199fn merge_all() {
200    use std::ops::Range;
201    assert_eq!(
202        SrcRef::merge_all(
203            [
204                SrcRef::new(Range { start: 5, end: 8 }, 1, 6, 123),
205                SrcRef::new(Range { start: 8, end: 10 }, 2, 1, 123),
206                SrcRef::new(Range { start: 12, end: 16 }, 3, 1, 123),
207                SrcRef::new(Range { start: 0, end: 10 }, 1, 1, 123),
208            ]
209            .iter(),
210        ),
211        SrcRef::new(Range { start: 0, end: 16 }, 1, 1, 123),
212    );
213}
214
215impl From<Pair<'_>> for SrcRef {
216    fn from(pair: Pair) -> Self {
217        let (line, col) = pair.line_col();
218        Self::new(
219            pair.as_span().start()..pair.as_span().end(),
220            line,
221            col,
222            pair.source_hash(),
223        )
224    }
225}
226
227#[test]
228fn test_src_ref() {
229    let input = "geo3d::Cube(size_x = 3.0, size_y = 3.0, size_z = 3.0);";
230
231    let cube = 7..11;
232    let size_y = 26..32;
233
234    let cube = SrcRef::new(cube, 1, 0, 0);
235    let size_y = SrcRef::new(size_y, 1, 0, 0);
236
237    assert_eq!(cube.source_slice(input), "Cube");
238    assert_eq!(size_y.source_slice(input), "size_y");
239}