Skip to main content

candela/compiler/
imports.rs

1//! Where an import resolves from.
2//!
3//! candela has one import form, a quoted path, and three places it can point:
4//! the file next to the one doing the importing, the standard library that
5//! ships with the toolchain, and a package the project depends on. This module
6//! holds the directories that answer the last two, so a compile is told where
7//! to look instead of reading the answer out of the environment each time it
8//! needs one.
9
10use smol_strc::SmolStr;
11use std::path::Path;
12use std::path::PathBuf;
13
14/// The entry a package uses when its manifest names none, relative to the
15/// package root. The same default `candela new` writes.
16pub const DEFAULT_PACKAGE_ENTRY: &str = "src/main.cdl";
17
18/// The directories a library import is looked up in.
19///
20/// `lib_dir` is the standard library, which every program can reach.
21/// `packages` are the packages this program depends on, each under the name
22/// the program imports it by. A package shadows the standard library for the
23/// name it claims, so `import "shapes/circle";` reaches the `shapes` package
24/// whether or not the toolchain ships a module by that name.
25#[derive(Clone, Debug)]
26pub struct ImportResolver {
27    lib_dir: Option<PathBuf>,
28    packages: Vec<PackageRoot>,
29}
30
31/// One package a program can import from.
32///
33/// `dir` is the package root, the directory its `candela.toml` sits in, and
34/// `entry` is the file the package is imported by under its own name, relative
35/// to that root. Imports of a path inside the package read from the entry's
36/// directory.
37#[derive(Clone, Debug)]
38struct PackageRoot {
39    name: SmolStr,
40    dir: PathBuf,
41    entry: PathBuf,
42}
43
44impl PackageRoot {
45    /// The directory a path inside the package is read from.
46    fn source_dir(&self) -> PathBuf {
47        match self.entry.parent() {
48            Some(parent) if !parent.as_os_str().is_empty() => self.dir.join(parent),
49            _ => self.dir.clone(),
50        }
51    }
52}
53
54impl Default for ImportResolver {
55    fn default() -> Self {
56        Self {
57            lib_dir: default_lib_dir(),
58            packages: Vec::new(),
59        }
60    }
61}
62
63impl ImportResolver {
64    /// A resolver that reaches the standard library and nothing else.
65    #[must_use]
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Points library imports at `dir` instead of the shipped location.
71    ///
72    /// `dir` is the directory that holds `std/`, the same thing
73    /// `CANDELA_LIB_PATH` names.
74    pub fn set_lib_dir(&mut self, dir: PathBuf) {
75        self.lib_dir = Some(dir);
76    }
77
78    /// Makes `dir` the root of the package imported as `name`, entered
79    /// through `entry`.
80    ///
81    /// `entry` is the package's entry file relative to `dir`, the one its
82    /// manifest names or [`DEFAULT_PACKAGE_ENTRY`] when it names none.
83    /// `import "name";` reads that file, and `import "name/x";` reads `x.cdl`
84    /// from the entry's directory. Adding a name twice keeps the last
85    /// directory and entry given for it.
86    pub fn add_root(&mut self, name: &str, dir: PathBuf, entry: impl Into<PathBuf>) {
87        let entry = entry.into();
88        if let Some(existing) = self.packages.iter_mut().find(|p| p.name == name) {
89            existing.dir = dir;
90            existing.entry = entry;
91            return;
92        }
93        self.packages.push(PackageRoot {
94            name: SmolStr::from(name),
95            dir,
96            entry,
97        });
98    }
99
100    /// The standard library directory in effect.
101    #[must_use]
102    pub fn lib_dir(&self) -> Option<&Path> {
103        self.lib_dir.as_deref()
104    }
105
106    /// The package names and root directories, in the order they were added.
107    #[must_use]
108    pub fn roots(&self) -> Vec<(SmolStr, PathBuf)> {
109        self.packages
110            .iter()
111            .map(|p| (p.name.clone(), p.dir.clone()))
112            .collect()
113    }
114
115    /// The package root directories on their own, which is what a native
116    /// library search wants: a package that ships a `.so` under its root has
117    /// it found there.
118    #[must_use]
119    pub fn root_dirs(&self) -> Vec<PathBuf> {
120        self.packages.iter().map(|p| p.dir.clone()).collect()
121    }
122
123    /// Where the library import `path` reads from, or `None` when there is
124    /// nowhere to look.
125    ///
126    /// `path` carries the `.cdl` the parser appended, so `import "shapes";`
127    /// arrives as `shapes.cdl` and `import "shapes/circle";` as
128    /// `shapes/circle.cdl`. The leading segment names the package; the rest is
129    /// the path inside it, read from the directory of the package's entry, and
130    /// a package with no rest reads the entry itself.
131    #[must_use]
132    pub fn library_path(&self, path: &str) -> Option<PathBuf> {
133        let (head, rest) = split_first_segment(path);
134        if let Some(package) = self.packages.iter().find(|p| p.name == head) {
135            return Some(match rest {
136                Some(rest) => package.source_dir().join(rest),
137                None => package.dir.join(&package.entry),
138            });
139        }
140        self.lib_dir.as_ref().map(|dir| dir.join(path))
141    }
142}
143
144/// Splits an import path into the package it might name and the path inside
145/// that package, when there is one.
146///
147/// `shapes/circle.cdl` is the `shapes` package's `circle.cdl`;
148/// `shapes.cdl` is the `shapes` package itself.
149fn split_first_segment(path: &str) -> (&str, Option<&str>) {
150    match path.find(['/', '\\']) {
151        Some(sep) => (&path[..sep], Some(&path[sep + 1..])),
152        None => (path.strip_suffix(".cdl").unwrap_or(path), None),
153    }
154}
155
156/// The standard library directory a resolver starts with: `CANDELA_LIB_PATH`
157/// when it is set, and `libs/` beside the running executable otherwise, which
158/// is where the toolchain installs it.
159///
160/// The runtime answers the same question when an artifact re-opens one of the
161/// library's own dynamic libraries, so the answer lives there and a compile
162/// reads it from the same place.
163fn default_lib_dir() -> Option<PathBuf> {
164    crate::rt::std_lib_dir()
165}
166
167#[cfg(test)]
168mod tests {
169    use super::DEFAULT_PACKAGE_ENTRY;
170    use super::ImportResolver;
171    use std::path::Path;
172    use std::path::PathBuf;
173
174    fn resolver() -> ImportResolver {
175        let mut resolver = ImportResolver {
176            lib_dir: Some(PathBuf::from("/toolchain/libs")),
177            packages: Vec::new(),
178        };
179        resolver.add_root(
180            "shapes",
181            PathBuf::from("/cache/shapes/1.2.3"),
182            DEFAULT_PACKAGE_ENTRY,
183        );
184        resolver
185    }
186
187    #[test]
188    fn a_std_path_reads_from_the_library_directory() {
189        assert_eq!(
190            resolver().library_path("std/string.cdl").as_deref(),
191            Some(Path::new("/toolchain/libs/std/string.cdl"))
192        );
193    }
194
195    #[test]
196    fn a_package_by_its_own_name_reads_its_entry() {
197        assert_eq!(
198            resolver().library_path("shapes.cdl").as_deref(),
199            Some(Path::new("/cache/shapes/1.2.3/src/main.cdl"))
200        );
201    }
202
203    #[test]
204    fn a_path_inside_a_package_reads_from_the_entry_directory() {
205        assert_eq!(
206            resolver().library_path("shapes/circle.cdl").as_deref(),
207            Some(Path::new("/cache/shapes/1.2.3/src/circle.cdl"))
208        );
209        assert_eq!(
210            resolver().library_path("shapes/two/deep.cdl").as_deref(),
211            Some(Path::new("/cache/shapes/1.2.3/src/two/deep.cdl"))
212        );
213    }
214
215    #[test]
216    fn a_manifest_entry_moves_both_forms() {
217        let mut resolver = resolver();
218        resolver.add_root("geom", PathBuf::from("/cache/geom/0.3.1"), "lib/geom.cdl");
219        assert_eq!(
220            resolver.library_path("geom.cdl").as_deref(),
221            Some(Path::new("/cache/geom/0.3.1/lib/geom.cdl"))
222        );
223        assert_eq!(
224            resolver.library_path("geom/arc.cdl").as_deref(),
225            Some(Path::new("/cache/geom/0.3.1/lib/arc.cdl"))
226        );
227    }
228
229    #[test]
230    fn an_entry_at_the_root_reads_paths_from_the_root() {
231        let mut resolver = resolver();
232        resolver.add_root("flat", PathBuf::from("/cache/flat/1.0.0"), "flat.cdl");
233        assert_eq!(
234            resolver.library_path("flat.cdl").as_deref(),
235            Some(Path::new("/cache/flat/1.0.0/flat.cdl"))
236        );
237        assert_eq!(
238            resolver.library_path("flat/more.cdl").as_deref(),
239            Some(Path::new("/cache/flat/1.0.0/more.cdl"))
240        );
241    }
242
243    #[test]
244    fn a_package_root_shadows_the_library_directory() {
245        let mut resolver = resolver();
246        resolver.add_root(
247            "std",
248            PathBuf::from("/cache/std/9.9.9"),
249            DEFAULT_PACKAGE_ENTRY,
250        );
251        assert_eq!(
252            resolver.library_path("std/string.cdl").as_deref(),
253            Some(Path::new("/cache/std/9.9.9/src/string.cdl"))
254        );
255    }
256
257    #[test]
258    fn a_root_added_twice_keeps_the_last_directory() {
259        let mut resolver = resolver();
260        resolver.add_root("shapes", PathBuf::from("/cache/shapes/2.0.0"), "shapes.cdl");
261        assert_eq!(resolver.roots().len(), 1);
262        assert_eq!(
263            resolver.library_path("shapes.cdl").as_deref(),
264            Some(Path::new("/cache/shapes/2.0.0/shapes.cdl"))
265        );
266    }
267
268    #[test]
269    fn without_a_library_directory_only_packages_resolve() {
270        let mut resolver = ImportResolver {
271            lib_dir: None,
272            packages: Vec::new(),
273        };
274        assert_eq!(resolver.library_path("std/string.cdl"), None);
275        resolver.add_root(
276            "shapes",
277            PathBuf::from("/cache/shapes/1.2.3"),
278            DEFAULT_PACKAGE_ENTRY,
279        );
280        assert_eq!(
281            resolver.library_path("shapes.cdl").as_deref(),
282            Some(Path::new("/cache/shapes/1.2.3/src/main.cdl"))
283        );
284    }
285
286    #[test]
287    fn the_root_directories_come_back_in_the_order_they_went_in() {
288        let mut resolver = resolver();
289        resolver.add_root(
290            "geom",
291            PathBuf::from("/cache/geom/0.3.1"),
292            DEFAULT_PACKAGE_ENTRY,
293        );
294        assert_eq!(
295            resolver.root_dirs(),
296            vec![
297                PathBuf::from("/cache/shapes/1.2.3"),
298                PathBuf::from("/cache/geom/0.3.1")
299            ]
300        );
301    }
302}