Skip to main content

microcad_lang/resolve/
sources.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Source file cache
5
6use derive_more::Deref;
7use microcad_lang_base::{
8    GetSourceLocInfoByHash, HashId, ResourceLocation, SourceLocInfo, SrcReferrer,
9};
10
11use crate::{
12    lower::{LowerErrorsWithSource, ir},
13    resolve::*,
14};
15use std::{collections::HashMap, rc::Rc};
16
17/// Register of loaded source files and their syntax trees.
18///
19/// Source file definitions ([`SourceFile`]) are stored in a vector (`Vec<Rc<SourceFile>>`)
20/// and mapped by *hash*, *path* and *name* via index to this vector.
21///
22/// The *root model* (given at creation) will be stored but will only be accessible by hash and path
23/// but not by it's qualified name.
24#[derive(Deref)]
25pub struct Sources {
26    by_hash: HashMap<u64, usize>,
27    by_path: HashMap<std::path::PathBuf, usize>,
28    by_name: HashMap<ir::QualifiedName, usize>,
29
30    //root source file.
31    root: Rc<ir::Source>,
32
33    /// External source files.
34    #[deref]
35    pub source_files: Vec<Rc<ir::Source>>,
36
37    /// Search paths.
38    search_paths: Vec<std::path::PathBuf>,
39}
40
41impl Sources {
42    /// Create source cache
43    ///
44    /// Inserts the `root` file and loads all files from `search_paths`.
45    pub fn load(
46        root: Rc<ir::Source>,
47        search_paths: Vec<std::path::PathBuf>,
48    ) -> ResolveResult<Self> {
49        let mut source_files = Vec::new();
50        let mut by_name = HashMap::new();
51        let mut by_hash = HashMap::new();
52        let mut by_path = HashMap::new();
53
54        by_hash.insert(root.source_hash(), 0);
55        by_path.insert(root.filename(), 0);
56        by_name.insert(root.name.clone(), 0);
57        source_files.push(root.clone());
58
59        // load all external source files into cache
60        Externals::new(&search_paths)?.iter().try_for_each(
61            |(name, path)| -> Result<(), LowerErrorsWithSource> {
62                let (source_file, error) = ir::Source::load_with_name(path.clone(), name.clone());
63                let index = source_files.len();
64                by_hash.insert(source_file.source_hash(), index);
65                by_path.insert(source_file.filename(), index);
66                by_name.insert(name.clone(), index);
67                source_files.push(source_file);
68                match error {
69                    Some(error) => Err(error),
70                    None => Ok(()),
71                }
72            },
73        )?;
74
75        Ok(Self {
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.canonicalize()
85                        .unwrap_or_else(|_| panic!("valid path: {}", path.display()))
86                })
87                .collect(),
88        })
89    }
90
91    /// Return root file.
92    pub fn root(&self) -> Rc<ir::Source> {
93        self.root.clone()
94    }
95
96    /// Insert a file to the sources.
97    pub fn insert(&mut self, source_file: Rc<ir::Source>) {
98        let hash = source_file.source_hash();
99        let path = source_file.filename();
100        let name = source_file.name.clone();
101
102        // maybe overwrite existing
103        let index = if let Some(index) = self.by_path.get(&path).copied() {
104            self.by_hash.remove(&hash);
105            self.by_name.remove(&name);
106            self.by_path.remove(&path);
107            if self.root.filename() == path {
108                self.root = source_file.clone();
109            }
110            self.source_files[index] = source_file;
111
112            index
113        } else {
114            self.source_files.push(source_file.clone());
115            self.source_files.len() - 1
116        };
117
118        self.by_hash.insert(hash, index);
119        self.by_path.insert(path, index);
120        self.by_name.insert(name, index);
121    }
122
123    /// Return the qualified name of a file by it's path
124    pub fn generate_name_from_path(
125        &self,
126        file_path: &std::path::Path,
127    ) -> ResolveResult<ir::QualifiedName> {
128        // check root file name
129        if self.root.filename() == file_path {
130            return Ok(self.root.id().into());
131        }
132
133        // check file names relative to search paths
134        let path = if let Some(path) = self
135            .search_paths
136            .iter()
137            .find_map(|path| file_path.strip_prefix(path).ok())
138        {
139            path.with_extension("")
140        }
141        // check file names relative to project root directory
142        else if let Some(root_dir) = self.root_dir() {
143            if let Ok(path) = file_path.strip_prefix(root_dir) {
144                path.with_extension("")
145            } else {
146                return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
147            }
148        } else {
149            return Err(ResolveError::InvalidPath(file_path.to_path_buf()));
150        };
151
152        // Remove prefix in testing environment
153        let path = if let Ok(path) = path.strip_prefix(".test") {
154            path.to_path_buf()
155        } else {
156            path
157        };
158
159        // check if file is a mod file then it gets it"s name from the parent directory
160        let path = if path
161            .iter()
162            .next_back()
163            .map(|s| s.to_string_lossy().to_string())
164            == Some("mod".into())
165        {
166            path.parent()
167        } else {
168            Some(path.as_path())
169        };
170
171        // get name from path which was found
172        if let Some(path) = path {
173            Ok(path
174                .iter()
175                .map(|name| ir::Identifier::no_ref(name.to_string_lossy().as_ref()))
176                .collect())
177        } else {
178            Err(ResolveError::InvalidPath(file_path.to_path_buf()))
179        }
180    }
181
182    /// Convenience function to get a source file by from a `SrcReferrer`.
183    pub fn get_by_src_ref(&self, referrer: &impl SrcReferrer) -> ResolveResult<Rc<ir::Source>> {
184        self.get_by_hash(referrer.src_ref().source_hash())
185    }
186
187    /// Return code at referrer.
188    pub fn get_code(&self, referrer: &impl SrcReferrer) -> ResolveResult<String> {
189        Ok(self
190            .get_by_src_ref(referrer)?
191            .get_code(&referrer.src_ref())
192            .to_string())
193    }
194
195    fn name_from_index(&self, index: usize) -> Option<ir::QualifiedName> {
196        self.by_name
197            .iter()
198            .find(|(_, i)| **i == index)
199            .map(|(name, _)| name.clone())
200    }
201
202    /// Return search paths of this cache.
203    pub fn search_paths(&self) -> &Vec<std::path::PathBuf> {
204        &self.search_paths
205    }
206
207    fn root_dir(&self) -> Option<std::path::PathBuf> {
208        self.root.filename().parent().map(|p| p.to_path_buf())
209    }
210
211    /// Load another source file into cache.
212    pub fn load_mod_file(
213        &mut self,
214        parent_path: impl AsRef<std::path::Path>,
215        id: &ir::Identifier,
216    ) -> ResolveResult<Rc<ir::Source>> {
217        log::trace!(
218            "loading file: {:?} [{id}]",
219            parent_path.as_ref().canonicalize().expect("invalid path")
220        );
221        let file_path = find_mod_file_by_id(parent_path, id)?;
222        let name = self.generate_name_from_path(&file_path)?;
223        let (source_file, error) = ir::Source::load_with_name(&file_path, name);
224        self.insert(source_file.clone());
225        match error {
226            Some(error) => Err(error.into()),
227            None => Ok(source_file),
228        }
229    }
230}
231
232/// Trait that can fetch for a file by it's hash value.
233pub trait GetSourceByHash {
234    /// Find a project file by it's hash value.
235    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<ir::Source>>;
236}
237
238impl GetSourceLocInfoByHash for Sources {
239    fn get_source_loc_info_by_hash(&'_ self, hash: HashId) -> Option<SourceLocInfo<'_>> {
240        self.by_hash
241            .get(&hash)
242            .map(|index| self.source_files[*index].source_loc_info())
243    }
244}
245
246impl GetSourceByHash for Sources {
247    /// Find a project file by it's hash value.
248    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<ir::Source>> {
249        if let Some(index) = self.by_hash.get(&hash) {
250            Ok(self.source_files[*index].clone())
251        } else if hash == 0 {
252            Err(ResolveError::NulHash)
253        } else {
254            Err(ResolveError::UnknownHash(hash))
255        }
256    }
257}
258
259impl std::fmt::Display for Sources {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        for (index, source_file) in self.source_files.iter().enumerate() {
262            let filename = source_file
263                .to_file_path()
264                .map(|p| p.to_string_lossy().to_string())
265                .unwrap_or("NO FILE".to_string());
266            let name = self.name_from_index(index).unwrap_or_default();
267            let hash = source_file.source_hash();
268            writeln!(f, "[{index}] {name} {hash:#x} {filename}")?;
269        }
270        Ok(())
271    }
272}
273
274impl std::fmt::Debug for Sources {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        for (index, source_file) in self.source_files.iter().enumerate() {
277            let filename = source_file
278                .to_file_path()
279                .map(|p| p.to_string_lossy().to_string())
280                .unwrap_or("NO FILE".to_string());
281            let name = self.name_from_index(index).unwrap_or_default();
282            let hash = source_file.source_hash();
283            writeln!(f, "[{index}] {name:?} {hash:#x} {filename}")?;
284        }
285        Ok(())
286    }
287}