microcad_lang/resolve/
sources.rs

1// Copyright © 2025 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<(), ParseError> {
65                let source_file = 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                Ok(())
72            })?;
73
74        Ok(Self {
75            externals,
76            root,
77            source_files,
78            by_hash,
79            by_path,
80            by_name,
81            search_paths: search_paths
82                .iter()
83                .map(|path| {
84                    path.as_ref()
85                        .canonicalize()
86                        .unwrap_or_else(|_| panic!("valid path: {}", path.as_ref().display()))
87                })
88                .collect(),
89        })
90    }
91
92    /// Return root file.
93    pub fn root(&self) -> Rc<SourceFile> {
94        self.root.clone()
95    }
96
97    /// Insert a file to the sources.
98    pub fn insert(&mut self, source_file: Rc<SourceFile>) {
99        let hash = source_file.hash;
100        let path = source_file.filename();
101        let name = source_file.name.clone();
102
103        // maybe overwrite existing
104        let index = if let Some(index) = self.by_path.get(&source_file.filename()).copied() {
105            self.by_hash.remove(&hash);
106            self.by_name.remove(&name);
107            self.by_path.remove(&path);
108            if self.root.filename() == path {
109                self.root = source_file.clone();
110            }
111            self.source_files[index] = source_file;
112
113            index
114        } else {
115            self.source_files.push(source_file.clone());
116            self.source_files.len() - 1
117        };
118
119        self.by_hash.insert(hash, index);
120        self.by_path.insert(path, index);
121        self.by_name.insert(name, index);
122    }
123
124    /// Return the qualified name of a file by it's path
125    pub fn generate_name_from_path(
126        &self,
127        file_path: &std::path::Path,
128    ) -> ResolveResult<QualifiedName> {
129        // check root file name
130        if self.root.filename() == file_path {
131            return Ok(QualifiedName::from_id(self.root.id()));
132        }
133
134        // check file names relative to search paths
135        let path = if let Some(path) = self
136            .search_paths
137            .iter()
138            .find_map(|path| file_path.strip_prefix(path).ok())
139        {
140            path.with_extension("")
141        }
142        // check file names relative to project root directory
143        else if let Some(root_dir) = self.root_dir() {
144            if let Ok(path) = file_path.strip_prefix(root_dir) {
145                path.with_extension("")
146            } else {
147                return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
148            }
149        } else {
150            return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
151        };
152
153        // Remove prefix in testing environment
154        let path = if let Ok(path) = path.strip_prefix(".test") {
155            path.to_path_buf()
156        } else {
157            path
158        };
159
160        // check if file is a mod file then it gets it"s name from the parent directory
161        let path = if path
162            .iter()
163            .next_back()
164            .map(|s| s.to_string_lossy().to_string())
165            == Some("mod".into())
166        {
167            path.parent()
168        } else {
169            Some(path.as_path())
170        };
171
172        // get name from path which was found
173        if let Some(path) = path {
174            Ok(path
175                .iter()
176                .map(|name| Identifier::no_ref(name.to_string_lossy().as_ref()))
177                .collect())
178        } else {
179            Err(ResolveError::InvalidPath(file_path.to_path_buf()))
180        }
181    }
182
183    /// Convenience function to get a source file by from a `SrcReferrer`.
184    pub fn get_by_src_ref(&self, referrer: &impl SrcReferrer) -> ResolveResult<Rc<SourceFile>> {
185        self.get_by_hash(referrer.src_ref().source_hash())
186    }
187
188    /// Return a string describing the given source code position.
189    pub fn ref_str(&self, referrer: &impl SrcReferrer) -> String {
190        format!(
191            "{}:{}",
192            self.get_by_src_ref(referrer)
193                .expect("Source file not found")
194                .filename_as_str(),
195            referrer.src_ref(),
196        )
197    }
198
199    /// Find a project file by it's file path.
200    pub fn get_by_path(&self, path: &std::path::Path) -> ResolveResult<Rc<SourceFile>> {
201        let path = path.to_path_buf();
202        if let Some(index) = self.by_path.get(&path) {
203            Ok(self.source_files[*index].clone())
204        } else {
205            Err(ResolveError::FileNotFound(path))
206        }
207    }
208
209    /// Get *qualified name* of a file by *hash value*.
210    pub fn get_name_by_hash(&self, hash: u64) -> ResolveResult<&QualifiedName> {
211        match self.get_by_hash(hash) {
212            Ok(file) => self.externals.get_name(&file.filename()),
213            Err(err) => Err(err),
214        }
215    }
216
217    /// Return code at referrer.
218    pub fn get_code(&self, referrer: &impl SrcReferrer) -> ResolveResult<String> {
219        Ok(self
220            .get_by_src_ref(referrer)?
221            .get_code(&referrer.src_ref())
222            .to_string())
223    }
224
225    /// Find a project file by the qualified name which represents the file path.
226    pub fn get_by_name(&self, name: &QualifiedName) -> ResolveResult<Rc<SourceFile>> {
227        if let Some(index) = self.by_name.get(name) {
228            Ok(self.source_files[*index].clone())
229        } else {
230            // if not found in symbol tree we try to find an external file to load
231            match self.externals.fetch_external(name) {
232                Ok((name, path)) => {
233                    if self.get_by_path(&path).is_err() {
234                        return Err(ResolveError::SymbolMustBeLoaded(name, path));
235                    }
236                }
237                Err(ResolveError::ExternalSymbolNotFound(_)) => (),
238                Err(err) => return Err(err),
239            }
240            Err(ResolveError::SymbolNotFound(name.clone()))
241        }
242    }
243
244    fn name_from_index(&self, index: usize) -> Option<QualifiedName> {
245        self.by_name
246            .iter()
247            .find(|(_, i)| **i == index)
248            .map(|(name, _)| name.clone())
249    }
250
251    /// Return search paths of this cache.
252    pub fn search_paths(&self) -> &Vec<std::path::PathBuf> {
253        &self.search_paths
254    }
255
256    fn root_dir(&self) -> Option<std::path::PathBuf> {
257        self.root.filename().parent().map(|p| p.to_path_buf())
258    }
259
260    /// Load another source file into cache.
261    pub fn load_mod_file(
262        &mut self,
263        parent_path: impl AsRef<std::path::Path>,
264        id: &Identifier,
265    ) -> ResolveResult<Rc<SourceFile>> {
266        log::trace!(
267            "loading file: {:?} [{id}]",
268            parent_path.as_ref().canonicalize().expect("invalid path")
269        );
270        let file_path = find_mod_file_by_id(parent_path, id)?;
271        let name = self.generate_name_from_path(&file_path)?;
272        let source_file = SourceFile::load_with_name(&file_path, name)?;
273        self.insert(source_file.clone());
274        Ok(source_file)
275    }
276
277    /// Reload an existing file
278    pub(super) fn update_file(
279        &mut self,
280        path: impl AsRef<std::path::Path>,
281    ) -> ResolveResult<ReplacedSourceFile> {
282        let path = path.as_ref().canonicalize()?.to_path_buf();
283        log::trace!("update_file: {path:?}");
284        if let Some(index) = self.by_path.get(&path).copied() {
285            let old = self.source_files[index].clone();
286            let name = old.name.clone();
287            let new = SourceFile::load_with_name(path, name)?;
288            self.insert(new.clone());
289            log::trace!("new sources:\n{self:?}");
290            Ok(ReplacedSourceFile { new, old })
291        } else {
292            Err(ResolveError::FileNotFound(path))
293        }
294    }
295}
296
297pub(super) struct ReplacedSourceFile {
298    pub(super) old: Rc<SourceFile>,
299    pub(super) new: Rc<SourceFile>,
300}
301
302/// Trait that can fetch for a file by it's hash value.
303pub trait GetSourceByHash {
304    /// Find a project file by it's hash value.
305    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>>;
306}
307
308impl GetSourceByHash for Sources {
309    /// Find a project file by it's hash value.
310    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>> {
311        if let Some(index) = self.by_hash.get(&hash) {
312            Ok(self.source_files[*index].clone())
313        } else if hash == 0 {
314            Err(ResolveError::NulHash)
315        } else {
316            Err(ResolveError::UnknownHash(hash))
317        }
318    }
319}
320
321impl std::fmt::Display for Sources {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        for (index, source_file) in self.source_files.iter().enumerate() {
324            let filename = source_file.filename_as_str();
325            let name = self
326                .name_from_index(index)
327                .unwrap_or(QualifiedName::no_ref(vec![]));
328            let hash = source_file.hash;
329            writeln!(f, "[{index}] {name} {hash:#x} {filename}")?;
330        }
331        Ok(())
332    }
333}
334
335impl std::fmt::Debug for Sources {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        for (index, source_file) in self.source_files.iter().enumerate() {
338            let filename = source_file.filename_as_str();
339            let name = self
340                .name_from_index(index)
341                .unwrap_or(QualifiedName::no_ref(vec![]));
342            let hash = source_file.hash;
343            writeln!(f, "[{index}] {name:?} {hash:#x} {filename}")?;
344        }
345        Ok(())
346    }
347}