Skip to main content

microcad_lang/resolve/
sources.rs

1// Copyright © 2025-2026 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Source file cache
5
6use derive_more::Deref;
7
8use crate::{parse::*, rc::*, resolve::*, src_ref::*, syntax::*};
9use std::collections::HashMap;
10
11/// Register of loaded source files and their syntax trees.
12///
13/// Source file definitions ([`SourceFile`]) are stored in a vector (`Vec<Rc<SourceFile>>`)
14/// and mapped by *hash*, *path* and *name* via index to this vector.
15///
16/// The *root model* (given at creation) will be stored but will only be accessible by hash and path
17/// but not by it's qualified name.
18#[derive(Default, Deref)]
19pub struct Sources {
20    /// External files read from search path.
21    externals: Externals,
22
23    by_hash: HashMap<u64, usize>,
24    by_path: HashMap<std::path::PathBuf, usize>,
25    by_name: HashMap<QualifiedName, usize>,
26
27    //root source file.
28    root: Rc<SourceFile>,
29
30    /// External source files.
31    #[deref]
32    pub source_files: Vec<Rc<SourceFile>>,
33
34    /// Search paths.
35    search_paths: Vec<std::path::PathBuf>,
36}
37
38impl Sources {
39    /// Create source cache
40    ///
41    /// Inserts the `root` file and loads all files from `search_paths`.
42    pub fn load(
43        root: Rc<SourceFile>,
44        search_paths: &[impl AsRef<std::path::Path>],
45    ) -> ResolveResult<Self> {
46        let mut source_files = Vec::new();
47        let mut by_name = HashMap::new();
48        let mut by_hash = HashMap::new();
49        let mut by_path = HashMap::new();
50
51        by_hash.insert(root.hash, 0);
52        by_path.insert(root.filename(), 0);
53        by_name.insert(root.name.clone(), 0);
54        source_files.push(root.clone());
55
56        // search for external source files
57        let externals = Externals::new(search_paths)?;
58
59        log::trace!("Externals:\n{externals}");
60
61        // load all external source files into cache
62        externals
63            .iter()
64            .try_for_each(|(name, path)| -> Result<(), ParseErrorsWithSource> {
65                let (source_file, error) = SourceFile::load_with_name(path.clone(), name.clone());
66                let index = source_files.len();
67                by_hash.insert(source_file.hash, index);
68                by_path.insert(source_file.filename(), index);
69                by_name.insert(name.clone(), index);
70                source_files.push(source_file);
71                match error {
72                    Some(error) => Err(error),
73                    None => Ok(()),
74                }
75            })?;
76
77        Ok(Self {
78            externals,
79            root,
80            source_files,
81            by_hash,
82            by_path,
83            by_name,
84            search_paths: search_paths
85                .iter()
86                .map(|path| {
87                    path.as_ref()
88                        .canonicalize()
89                        .unwrap_or_else(|_| panic!("valid path: {}", path.as_ref().display()))
90                })
91                .collect(),
92        })
93    }
94
95    /// Return root file.
96    pub fn root(&self) -> Rc<SourceFile> {
97        self.root.clone()
98    }
99
100    /// Insert a file to the sources.
101    pub fn insert(&mut self, source_file: Rc<SourceFile>) {
102        let hash = source_file.hash;
103        let path = source_file.filename();
104        let name = source_file.name.clone();
105
106        // maybe overwrite existing
107        let index = if let Some(index) = self.by_path.get(&path).copied() {
108            self.by_hash.remove(&hash);
109            self.by_name.remove(&name);
110            self.by_path.remove(&path);
111            if self.root.filename() == path {
112                self.root = source_file.clone();
113            }
114            self.source_files[index] = source_file;
115
116            index
117        } else {
118            self.source_files.push(source_file.clone());
119            self.source_files.len() - 1
120        };
121
122        self.by_hash.insert(hash, index);
123        self.by_path.insert(path, index);
124        self.by_name.insert(name, index);
125    }
126
127    /// Return the qualified name of a file by it's path
128    pub fn generate_name_from_path(
129        &self,
130        file_path: &std::path::Path,
131    ) -> ResolveResult<QualifiedName> {
132        // check root file name
133        if self.root.filename() == file_path {
134            return Ok(QualifiedName::from_id(self.root.id()));
135        }
136
137        // check file names relative to search paths
138        let path = if let Some(path) = self
139            .search_paths
140            .iter()
141            .find_map(|path| file_path.strip_prefix(path).ok())
142        {
143            path.with_extension("")
144        }
145        // check file names relative to project root directory
146        else if let Some(root_dir) = self.root_dir() {
147            if let Ok(path) = file_path.strip_prefix(root_dir) {
148                path.with_extension("")
149            } else {
150                return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
151            }
152        } else {
153            return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
154        };
155
156        // Remove prefix in testing environment
157        let path = if let Ok(path) = path.strip_prefix(".test") {
158            path.to_path_buf()
159        } else {
160            path
161        };
162
163        // check if file is a mod file then it gets it"s name from the parent directory
164        let path = if path
165            .iter()
166            .next_back()
167            .map(|s| s.to_string_lossy().to_string())
168            == Some("mod".into())
169        {
170            path.parent()
171        } else {
172            Some(path.as_path())
173        };
174
175        // get name from path which was found
176        if let Some(path) = path {
177            Ok(path
178                .iter()
179                .map(|name| Identifier::no_ref(name.to_string_lossy().as_ref()))
180                .collect())
181        } else {
182            Err(ResolveError::InvalidPath(file_path.to_path_buf()))
183        }
184    }
185
186    /// Convenience function to get a source file by from a `SrcReferrer`.
187    pub fn get_by_src_ref(&self, referrer: &impl SrcReferrer) -> ResolveResult<Rc<SourceFile>> {
188        self.get_by_hash(referrer.src_ref().source_hash())
189    }
190
191    /// Return a string describing the given source code position.
192    pub fn ref_str(&self, referrer: &impl SrcReferrer) -> String {
193        format!(
194            "{}:{}",
195            self.get_by_src_ref(referrer)
196                .expect("Source file not found")
197                .filename_as_str(),
198            referrer.src_ref(),
199        )
200    }
201
202    /// Find a project file by it's file path.
203    pub fn get_by_path(&self, path: &std::path::Path) -> ResolveResult<Rc<SourceFile>> {
204        let path = path.to_path_buf();
205        if let Some(index) = self.by_path.get(&path) {
206            Ok(self.source_files[*index].clone())
207        } else {
208            Err(ResolveError::FileNotFound(path))
209        }
210    }
211
212    /// Get *qualified name* of a file by *hash value*.
213    pub fn get_name_by_hash(&self, hash: u64) -> ResolveResult<&QualifiedName> {
214        match self.get_by_hash(hash) {
215            Ok(file) => self.externals.get_name(&file.filename()),
216            Err(err) => Err(err),
217        }
218    }
219
220    /// Return code at referrer.
221    pub fn get_code(&self, referrer: &impl SrcReferrer) -> ResolveResult<String> {
222        Ok(self
223            .get_by_src_ref(referrer)?
224            .get_code(&referrer.src_ref())
225            .to_string())
226    }
227
228    /// Find a project file by the qualified name which represents the file path.
229    pub fn get_by_name(&self, name: &QualifiedName) -> ResolveResult<Rc<SourceFile>> {
230        if let Some(index) = self.by_name.get(name) {
231            Ok(self.source_files[*index].clone())
232        } else {
233            // if not found in symbol tree we try to find an external file to load
234            match self.externals.fetch_external(name) {
235                Ok((name, path)) => {
236                    if self.get_by_path(&path).is_err() {
237                        return Err(ResolveError::SymbolMustBeLoaded(name, path));
238                    }
239                }
240                Err(ResolveError::ExternalSymbolNotFound(_)) => (),
241                Err(err) => return Err(err),
242            }
243            Err(ResolveError::SymbolNotFound(name.clone()))
244        }
245    }
246
247    fn name_from_index(&self, index: usize) -> Option<QualifiedName> {
248        self.by_name
249            .iter()
250            .find(|(_, i)| **i == index)
251            .map(|(name, _)| name.clone())
252    }
253
254    /// Return search paths of this cache.
255    pub fn search_paths(&self) -> &Vec<std::path::PathBuf> {
256        &self.search_paths
257    }
258
259    fn root_dir(&self) -> Option<std::path::PathBuf> {
260        self.root.filename().parent().map(|p| p.to_path_buf())
261    }
262
263    /// Load another source file into cache.
264    pub fn load_mod_file(
265        &mut self,
266        parent_path: impl AsRef<std::path::Path>,
267        id: &Identifier,
268    ) -> ResolveResult<Rc<SourceFile>> {
269        log::trace!(
270            "loading file: {:?} [{id}]",
271            parent_path.as_ref().canonicalize().expect("invalid path")
272        );
273        let file_path = find_mod_file_by_id(parent_path, id)?;
274        let name = self.generate_name_from_path(&file_path)?;
275        let (source_file, error) = SourceFile::load_with_name(&file_path, name);
276        self.insert(source_file.clone());
277        match error {
278            Some(error) => Err(error.into()),
279            None => Ok(source_file),
280        }
281    }
282
283    /// Reload an existing file
284    pub(super) fn update_file(
285        &mut self,
286        path: impl AsRef<std::path::Path>,
287    ) -> ResolveResult<ReplacedSourceFile> {
288        let path = path.as_ref().canonicalize()?.to_path_buf();
289        log::trace!("update_file: {path:?}");
290        if let Some(index) = self.by_path.get(&path).copied() {
291            let old = self.source_files[index].clone();
292            let name = old.name.clone();
293            let (new, error) = SourceFile::load_with_name(path, name);
294            self.insert(new.clone());
295            log::trace!("new sources:\n{self:?}");
296            match error {
297                Some(error) => Err(error.into()),
298                None => Ok(ReplacedSourceFile { new, old }),
299            }
300        } else {
301            Err(ResolveError::FileNotFound(path))
302        }
303    }
304}
305
306pub(super) struct ReplacedSourceFile {
307    pub(super) old: Rc<SourceFile>,
308    pub(super) new: Rc<SourceFile>,
309}
310
311/// Trait that can fetch for a file by it's hash value.
312pub trait GetSourceByHash {
313    /// Find a project file by it's hash value.
314    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>>;
315}
316
317impl GetSourceByHash for Sources {
318    /// Find a project file by it's hash value.
319    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>> {
320        if let Some(index) = self.by_hash.get(&hash) {
321            Ok(self.source_files[*index].clone())
322        } else if hash == 0 {
323            Err(ResolveError::NulHash)
324        } else {
325            Err(ResolveError::UnknownHash(hash))
326        }
327    }
328}
329
330impl std::fmt::Display for Sources {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        for (index, source_file) in self.source_files.iter().enumerate() {
333            let filename = source_file.filename_as_str();
334            let name = self
335                .name_from_index(index)
336                .unwrap_or(QualifiedName::no_ref(vec![]));
337            let hash = source_file.hash;
338            writeln!(f, "[{index}] {name} {hash:#x} {filename}")?;
339        }
340        Ok(())
341    }
342}
343
344impl std::fmt::Debug for Sources {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        for (index, source_file) in self.source_files.iter().enumerate() {
347            let filename = source_file.filename_as_str();
348            let name = self
349                .name_from_index(index)
350                .unwrap_or(QualifiedName::no_ref(vec![]));
351            let hash = source_file.hash;
352            writeln!(f, "[{index}] {name:?} {hash:#x} {filename}")?;
353        }
354        Ok(())
355    }
356}