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| path.as_ref().canonicalize().expect("valid path"))
84                .collect(),
85        })
86    }
87
88    /// Return root file.
89    pub fn root(&self) -> Rc<SourceFile> {
90        self.root.clone()
91    }
92
93    /// Insert a file to the sources.
94    pub fn insert(&mut self, source_file: Rc<SourceFile>) {
95        let index = self.source_files.len();
96        self.source_files.push(source_file.clone());
97        self.by_hash.insert(source_file.hash, index);
98        self.by_path.insert(source_file.filename(), index);
99        self.by_name.insert(source_file.name.clone(), index);
100    }
101
102    /// Return the qualified name of a file by it's path
103    pub fn generate_name_from_path(
104        &self,
105        file_path: &std::path::Path,
106    ) -> ResolveResult<QualifiedName> {
107        // check root file name
108        if self.root.filename() == file_path {
109            return Ok(QualifiedName::from_id(self.root.id()));
110        }
111
112        // check file names relative to search paths
113        let path = if let Some(path) = self
114            .search_paths
115            .iter()
116            .find_map(|path| file_path.strip_prefix(path).ok())
117        {
118            path.with_extension("")
119        }
120        // check file names relative to project root directory
121        else if let Some(root_dir) = self.root_dir() {
122            if let Ok(path) = file_path.strip_prefix(root_dir) {
123                path.with_extension("")
124            } else {
125                return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
126            }
127        } else {
128            return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
129        };
130
131        // check if file is a mod file then it gets it"s name from the parent directory
132        let path = if path
133            .iter()
134            .next_back()
135            .map(|s| s.to_string_lossy().to_string())
136            == Some("mod".into())
137        {
138            path.parent()
139        } else {
140            Some(path.as_path())
141        };
142
143        // get name from path which was found
144        if let Some(path) = path {
145            Ok(path
146                .iter()
147                .map(|name| Identifier::no_ref(name.to_string_lossy().as_ref()))
148                .collect())
149        } else {
150            Err(ResolveError::InvalidPath(file_path.to_path_buf()))
151        }
152    }
153
154    /// Convenience function to get a source file by from a `SrcReferrer`.
155    pub fn get_by_src_ref(&self, referrer: &impl SrcReferrer) -> ResolveResult<Rc<SourceFile>> {
156        self.get_by_hash(referrer.src_ref().source_hash())
157    }
158
159    /// Return a string describing the given source code position.
160    pub fn ref_str(&self, referrer: &impl SrcReferrer) -> String {
161        format!(
162            "{}:{}",
163            self.get_by_src_ref(referrer)
164                .expect("Source file not found")
165                .filename_as_str(),
166            referrer.src_ref(),
167        )
168    }
169
170    /// Find a project file by it's file path.
171    pub fn get_by_path(&self, path: &std::path::Path) -> ResolveResult<Rc<SourceFile>> {
172        let path = path.to_path_buf();
173        if let Some(index) = self.by_path.get(&path) {
174            Ok(self.source_files[*index].clone())
175        } else {
176            Err(ResolveError::FileNotFound(path))
177        }
178    }
179
180    /// Get *qualified name* of a file by *hash value*.
181    pub fn get_name_by_hash(&self, hash: u64) -> ResolveResult<&QualifiedName> {
182        match self.get_by_hash(hash) {
183            Ok(file) => self.externals.get_name(&file.filename()),
184            Err(err) => Err(err),
185        }
186    }
187
188    /// Find a project file by the qualified name which represents the file path.
189    pub fn get_by_name(&self, name: &QualifiedName) -> ResolveResult<Rc<SourceFile>> {
190        if let Some(index) = self.by_name.get(name) {
191            Ok(self.source_files[*index].clone())
192        } else {
193            // if not found in symbol tree we try to find an external file to load
194            match self.externals.fetch_external(name) {
195                Ok((name, path)) => {
196                    if self.get_by_path(&path).is_err() {
197                        return Err(ResolveError::SymbolMustBeLoaded(name, path));
198                    }
199                }
200                Err(ResolveError::ExternalSymbolNotFound(_)) => (),
201                Err(err) => return Err(err),
202            }
203            Err(ResolveError::SymbolNotFound(name.clone()))
204        }
205    }
206
207    fn name_from_index(&self, index: usize) -> Option<QualifiedName> {
208        self.by_name
209            .iter()
210            .find(|(_, i)| **i == index)
211            .map(|(name, _)| name.clone())
212    }
213
214    /// Return search paths of this cache.
215    pub fn search_paths(&self) -> &Vec<std::path::PathBuf> {
216        &self.search_paths
217    }
218
219    fn root_dir(&self) -> Option<std::path::PathBuf> {
220        self.root.filename().parent().map(|p| p.to_path_buf())
221    }
222
223    /// Load another source file into cache.
224    pub fn load_file(
225        &mut self,
226        parent_path: impl AsRef<std::path::Path>,
227        id: &Identifier,
228    ) -> ResolveResult<Rc<SourceFile>> {
229        log::trace!("load_file: {:?} {id}", parent_path.as_ref());
230        let file_path = find_mod_file_by_id(parent_path, id)?;
231        let name = self.generate_name_from_path(&file_path)?;
232        let source_file = SourceFile::load_with_name(&file_path, name)?;
233        self.insert(source_file.clone());
234        Ok(source_file)
235    }
236}
237
238/// Trait that can fetch for a file by it's hash value.
239pub trait GetSourceByHash {
240    /// Find a project file by it's hash value.
241    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>>;
242}
243
244impl GetSourceByHash for Sources {
245    /// Find a project file by it's hash value.
246    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>> {
247        if let Some(index) = self.by_hash.get(&hash) {
248            Ok(self.source_files[*index].clone())
249        } else if hash == 0 {
250            Err(ResolveError::NulHash)
251        } else {
252            Err(ResolveError::UnknownHash(hash))
253        }
254    }
255}
256
257impl std::fmt::Display for Sources {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        for (index, source_file) in self.source_files.iter().enumerate() {
260            let filename = source_file.filename_as_str();
261            let name = self
262                .name_from_index(index)
263                .unwrap_or(QualifiedName::no_ref(vec![]));
264            let hash = source_file.hash;
265            writeln!(f, "[{index}] {name} {hash:#x} {filename}")?;
266        }
267        Ok(())
268    }
269}
270
271impl std::fmt::Debug for Sources {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        for (index, source_file) in self.source_files.iter().enumerate() {
274            let filename = source_file.filename_as_str();
275            let name = self
276                .name_from_index(index)
277                .unwrap_or(QualifiedName::no_ref(vec![]));
278            let hash = source_file.hash;
279            writeln!(f, "[{index}] {name:?} {hash:#x} {filename}")?;
280        }
281        Ok(())
282    }
283}