Skip to main content

lutra_compiler/
project.rs

1use std::path;
2use std::str::FromStr;
3
4use indexmap::IndexMap;
5use itertools::Itertools;
6
7use crate::pr;
8
9/// Project, checked.
10#[derive(Debug)]
11pub struct Project {
12    /// Discovered sources
13    pub source: SourceTree,
14
15    /// Resolved definitions
16    pub root_module: pr::ModuleDef,
17
18    /// Resolution ordering of definitions
19    // TODO: make a more efficient "a ordered vec of unordered groups" data structure
20    pub ordering: Vec<Vec<pr::Path>>,
21
22    pub dependencies: Vec<Dependency>,
23}
24
25#[derive(Debug)]
26pub struct Dependency {
27    pub name: String,
28
29    pub inner: Project,
30}
31
32/// Sources used to resolve the project.
33/// All paths are relative to the project root.
34// We use `SourceTree` to represent both a single file (including a "file" piped
35// from stdin), and a collection of files. (Possibly this could be implemented
36// as a Trait with a Struct for each type, which would use structure over values
37// (i.e. `Option<PathBuf>` below signifies whether it's a project or not). But
38// waiting until it's necessary before splitting it out.)
39#[derive(Debug, Clone)]
40pub struct SourceTree {
41    /// Path to the root of the source tree.
42    /// Can be a directory that contains module.lt or a .lt file.
43    root: path::PathBuf,
44
45    /// Mapping from file paths into into their contents.
46    /// Paths are relative to the root.
47    sources: IndexMap<path::PathBuf, String>,
48}
49
50impl SourceTree {
51    pub fn empty() -> Self {
52        SourceTree {
53            sources: Default::default(),
54            root: path::PathBuf::new(),
55        }
56    }
57
58    pub fn single(path: path::PathBuf, content: String) -> Self {
59        SourceTree {
60            sources: [(path.clone(), content)].into(),
61            root: path::PathBuf::new(),
62        }
63    }
64
65    pub fn new<I>(iter: I, root: path::PathBuf) -> Self
66    where
67        I: IntoIterator<Item = (path::PathBuf, String)>,
68    {
69        SourceTree {
70            sources: IndexMap::from_iter(iter),
71            root,
72        }
73    }
74
75    pub fn is_empty(&self) -> bool {
76        self.sources.len() == 0
77    }
78
79    pub fn insert(&mut self, path: path::PathBuf, content: String) {
80        self.sources.insert(path, content);
81    }
82
83    pub fn replace(&mut self, path: &path::Path, content: String) -> Option<String> {
84        let source = self.sources.get_mut(path)?;
85        Some(std::mem::replace(source, content))
86    }
87
88    pub fn get_ids(&self) -> impl Iterator<Item = u16> {
89        0..self.sources.len() as u16
90    }
91    pub fn get_sources(&self) -> impl Iterator<Item = (&path::PathBuf, &String)> {
92        self.sources.iter()
93    }
94
95    pub fn get_by_id(&self, source_id: u16) -> Option<(&path::Path, &str)> {
96        self.sources
97            .get_index(source_id as usize)
98            .map(|(p, c)| (p.as_path(), c.as_str()))
99    }
100
101    pub fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
102        self.sources
103            .get_full(path)
104            .map(|(i, _, content)| (i as u16, content.as_str()))
105    }
106
107    pub fn get_source_display_paths(&self) -> impl Iterator<Item = &path::Path> {
108        self.sources
109            .keys()
110            .map(|path| self.get_display_path(path).unwrap())
111    }
112
113    /// Converts a "project path" into an absolute path in the file-system.
114    pub fn get_absolute_path(&self, path: impl AsRef<path::Path>) -> path::PathBuf {
115        let path = path.as_ref();
116        if path.as_os_str().is_empty() {
117            self.root.to_path_buf()
118        } else {
119            self.get_project_dir().join(path)
120        }
121    }
122
123    /// Converts an absolute path into a path relative to the root.
124    /// Not that this is not relative to "project dir".
125    pub fn get_relative_path<'a>(
126        &self,
127        absolute_path: &'a path::Path,
128    ) -> Result<&'a path::Path, path::StripPrefixError> {
129        absolute_path.strip_prefix(&self.root)
130    }
131
132    /// Returns project dir: the directory in which the project files reside.
133    /// For example,
134    /// - if root is `/some_path/project/`, then that is also the project dir,
135    /// - if root is `/some_path/my_file.lt`, then project dir is `/some_path/`.
136    pub fn get_project_dir(&self) -> &path::Path {
137        if self.root.extension().is_some_and(|e| e == "lt") {
138            self.root.parent().unwrap()
139        } else {
140            &self.root
141        }
142    }
143
144    /// Converts a path (either absolute or relative to project root) into a
145    /// "display path", which is path relative to "project dir".
146    /// This is equivalent to "relative path", except for single-file projects,
147    /// where root is a file and "project dir" is its parent directory.
148    pub fn get_display_path<'a>(&'a self, path: &'a path::Path) -> Option<&'a path::Path> {
149        if path.is_absolute() {
150            path.strip_prefix(self.get_project_dir()).ok()
151        } else if path.as_os_str().is_empty() {
152            self.root.file_name().map(path::Path::new)
153        } else {
154            Some(path)
155        }
156    }
157}
158
159impl std::fmt::Display for SourceTree {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        let mut r = format!("path: {}\nsources:\n", self.root.to_string_lossy());
162
163        for source in self.sources.keys().sorted() {
164            r += "- ";
165            r += &source.to_string_lossy();
166            r += "\n";
167        }
168
169        f.write_str(&r)
170    }
171}
172
173/// Project source (in [SourceTree]) and a code snippet
174pub struct SourceOverlay<'a> {
175    tree: &'a SourceTree,
176
177    snippet_path: path::PathBuf,
178    snippet: &'a str,
179}
180
181impl<'a> SourceOverlay<'a> {
182    pub fn new(tree: &'a SourceTree, snippet: &'a str, snippet_path: Option<&str>) -> Self {
183        Self {
184            tree,
185            snippet,
186            snippet_path: snippet_path
187                .map(|s| path::PathBuf::from_str(s).unwrap())
188                .unwrap_or_default(),
189        }
190    }
191
192    pub const fn overlay_id() -> u16 {
193        u16::MAX
194    }
195}
196
197pub(crate) trait SourceProvider {
198    fn get_root(&self) -> &path::Path;
199
200    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)>;
201
202    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)>;
203}
204
205impl SourceProvider for SourceTree {
206    fn get_root(&self) -> &path::Path {
207        &self.root
208    }
209    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)> {
210        SourceTree::get_by_id(self, id)
211    }
212
213    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
214        SourceTree::get_by_path(self, path)
215    }
216}
217
218impl<'a> SourceProvider for SourceOverlay<'a> {
219    fn get_root(&self) -> &path::Path {
220        &self.tree.root
221    }
222
223    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)> {
224        if id == SourceOverlay::overlay_id() {
225            Some((self.snippet_path.as_path(), self.snippet))
226        } else {
227            self.tree.get_by_id(id)
228        }
229    }
230
231    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
232        if path == self.snippet_path {
233            Some((SourceOverlay::overlay_id(), self.snippet))
234        } else {
235            self.tree.get_by_path(path)
236        }
237    }
238}