Skip to main content

adam_lang/
source_map.rs

1//! Owning source identities for diagnostics across module collections.
2//!
3//! A location retains its immutable file name and text even after its source map
4//! is dropped. Native definitions use labels with no location.
5//!
6//! ```
7//! use adam_lang::{SourceMap, SourceLabel, SourceLocation};
8//! use cel_parser::SourceSpan;
9//! let mut sources = SourceMap::new();
10//! let source = sources.insert("example.adm2", "sheet example {}");
11//! let label = SourceLabel {
12//!     location: Some(SourceLocation { source, span: SourceSpan::new(1, 0, 1, 5) }),
13//!     label: "sheet declaration".into(),
14//! };
15//! drop(sources);
16//! assert_eq!(label.location.unwrap().source.file_name(), "example.adm2");
17//! ```
18
19use std::hash::{Hash, Hasher};
20use std::rc::Rc;
21
22use cel_parser::SourceSpan;
23
24/// Retains one immutable file name and its complete source text.
25#[derive(Debug)]
26struct SourceFile {
27    file_name: String,
28    text: String,
29}
30
31/// Identifies and owns one source independently of its collection's lifetime.
32///
33/// Equality and hashing use immutable ownership identity, not file contents.
34/// Independent insertions remain distinct even with identical names and text.
35/// See the [module example](self).
36#[derive(Clone, Debug)]
37pub struct SourceId {
38    file: Rc<SourceFile>,
39    index: usize,
40}
41
42impl PartialEq for SourceId {
43    /// Compares immutable source ownership.
44    fn eq(&self, other: &Self) -> bool {
45        Rc::ptr_eq(&self.file, &other.file)
46    }
47}
48
49impl Eq for SourceId {}
50
51impl Hash for SourceId {
52    /// Hashes the same ownership identity used by equality.
53    fn hash<H: Hasher>(&self, state: &mut H) {
54        Rc::as_ptr(&self.file).hash(state);
55    }
56}
57
58impl SourceId {
59    /// Borrows the retained file name without consulting a collection or filesystem.
60    ///
61    /// # Examples
62    ///
63    /// See the [module example](self).
64    pub fn file_name(&self) -> &str {
65        &self.file.file_name
66    }
67
68    /// Borrows the complete retained source text.
69    ///
70    /// # Examples
71    ///
72    /// See the [module example](self).
73    pub fn text(&self) -> &str {
74        &self.file.text
75    }
76}
77
78/// Collects immutable source owners in insertion order without filesystem IO.
79///
80/// Locations retain their own owners; dropping this collection does not invalidate
81/// them. See the [module example](self).
82#[derive(Debug, Default)]
83pub struct SourceMap {
84    sources: Vec<SourceId>,
85}
86
87impl SourceMap {
88    /// Creates an empty source collection.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// assert!(adam_lang::SourceMap::new().sources().is_empty());
94    /// ```
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Copies a file name and source text into a new immutable source owner.
100    ///
101    /// Repeated insertions create distinct identities.
102    /// - Complexity: O(f + t) time and space for file name length f and text length t,
103    ///   plus amortized O(1) collection insertion.
104    ///
105    /// # Examples
106    ///
107    /// See the [module example](self).
108    pub fn insert(&mut self, file_name: &str, text: &str) -> SourceId {
109        let id = SourceId {
110            file: Rc::new(SourceFile {
111                file_name: file_name.into(),
112                text: text.into(),
113            }),
114            index: self.sources.len(),
115        };
116        self.sources.push(id.clone());
117        id
118    }
119
120    /// Returns the retained owner only if this collection contains `source`.
121    ///
122    /// Coincident indices in independent collections do not match.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// let mut map = adam_lang::SourceMap::new();
128    /// let id = map.insert("file.adm2", "");
129    /// assert_eq!(map.get(&id), Some(&id));
130    /// assert!(adam_lang::SourceMap::new().get(&id).is_none());
131    /// ```
132    pub fn get(&self, source: &SourceId) -> Option<&SourceId> {
133        self.sources.get(source.index).filter(|id| *id == source)
134    }
135
136    /// Borrows source owners in insertion order.
137    ///
138    /// # Examples
139    ///
140    /// See [`Self::new`].
141    pub fn sources(&self) -> &[SourceId] {
142        &self.sources
143    }
144}
145
146/// Retains a real source owner and its line/column span.
147///
148/// See the [module example](self).
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct SourceLocation {
151    /// Owns the file name and text to which `span` refers.
152    pub source: SourceId,
153    /// Identifies the region within the retained source text.
154    pub span: SourceSpan,
155}
156
157/// Labels a source region, or a source-free native definition without a location.
158///
159/// See the [module example](self).
160#[derive(Clone, Debug, Eq, PartialEq)]
161pub struct SourceLabel {
162    /// Contains a real source location, or `None` for source-free definitions.
163    pub location: Option<SourceLocation>,
164    /// Describes the implicated definition or use.
165    pub label: String,
166}
167
168/// Owns a module error message and all sources needed by its labels.
169///
170/// ```
171/// let diagnostic = adam_lang::ModuleDiagnostic {
172///     message: "invalid native use".into(),
173///     labels: vec![adam_lang::SourceLabel { location: None, label: "native".into() }],
174///     cause: None,
175/// };
176/// assert!(diagnostic.labels[0].location.is_none());
177/// ```
178#[derive(Clone, Debug, Eq, PartialEq)]
179pub struct ModuleDiagnostic {
180    /// Explains the failed module operation.
181    pub message: String,
182    /// Identifies implicated definitions and uses in diagnostic order.
183    pub labels: Vec<SourceLabel>,
184    /// Retains an underlying construction error without replacing it with display text.
185    pub cause: Option<ModuleCause>,
186}
187
188/// Shares an immutable diagnostic cause with identity-based equality.
189///
190/// Clones retain the same error identity; separately constructed causes remain distinct.
191/// See [`ModuleDiagnostic`].
192#[derive(Clone, Debug)]
193pub struct ModuleCause(std::rc::Rc<dyn std::error::Error>);
194
195impl ModuleCause {
196    /// Retains a concrete error, including non-Send native template ownership handles.
197    ///
198    /// # Examples
199    /// ```
200    /// let cause = adam_lang::source_map::ModuleCause::new(std::io::Error::other("failure"));
201    /// assert_eq!(cause, cause.clone());
202    /// ```
203    pub fn new(error: impl std::error::Error + 'static) -> Self {
204        Self(std::rc::Rc::new(error))
205    }
206
207    /// Borrows the original error for source traversal and concrete downcasting.
208    ///
209    /// # Examples
210    /// ```
211    /// let cause = adam_lang::source_map::ModuleCause::new(std::io::Error::other("failure"));
212    /// assert!(cause.as_error().is::<std::io::Error>());
213    /// ```
214    pub fn as_error(&self) -> &(dyn std::error::Error + 'static) {
215        self.0.as_ref()
216    }
217}
218
219impl PartialEq for ModuleCause {
220    /// Compares immutable error identity, not display text.
221    fn eq(&self, other: &Self) -> bool {
222        std::rc::Rc::ptr_eq(&self.0, &other.0)
223    }
224}
225
226impl Eq for ModuleCause {}
227
228impl std::fmt::Display for ModuleDiagnostic {
229    /// Formats the primary diagnostic message without rendering source labels.
230    ///
231    /// - Complexity: O(m) message bytes.
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        f.write_str(&self.message)
234    }
235}
236
237impl std::error::Error for ModuleDiagnostic {
238    /// Borrows the retained original error, if this diagnostic wraps a construction failure.
239    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
240        self.cause.as_ref().map(ModuleCause::as_error)
241    }
242}