Skip to main content

knave_expand/modules/
sources.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use super::libraries::{LibraryRoot, LibraryTable};
6use crate::core::ModulePath;
7use crate::language::Language;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum BuiltinModule {
11    Kernel,
12    Code,
13}
14
15pub enum ModuleSource {
16    Builtin(BuiltinModule),
17    Text { name: String, text: String },
18}
19
20pub struct LocalModuleClash<'s> {
21    pub local: PathBuf,
22    pub library: &'s str,
23    pub lister: &'s str,
24}
25
26pub struct HiddenSourceModule {
27    pub hiding: PathBuf,
28    pub hidden: PathBuf,
29}
30
31pub struct ModuleSources {
32    root: PathBuf,
33    display_root: PathBuf,
34    fallback_root: Option<PathBuf>,
35    libraries: LibraryTable,
36}
37
38impl ModuleSources {
39    pub const KERNEL: &str = "kernel";
40    pub const CODE: &str = "code";
41    pub const PRELUDE: &str = "prelude";
42    pub const TEST: &str = "prelude/test";
43    pub const PROJECT_SHAPES: &str = "prelude/project-file";
44    const PRELUDE_DIRECTORY: &str = "prelude/";
45    const INTERNAL_DIRECTORY: &str = "prelude/internal/";
46    const BYTE_ORDER_MARK: char = '\u{feff}';
47    const PRELUDE_FILES: [(&str, &str); 24] = [
48        ("prelude", include_str!("../../prelude/prelude.rslisp")),
49        (
50            "prelude/internal/macros",
51            include_str!("../../prelude/internal/macros.rslisp"),
52        ),
53        (
54            "prelude/macros",
55            include_str!("../../prelude/macros.rslisp"),
56        ),
57        ("prelude/logic", include_str!("../../prelude/logic.rslisp")),
58        (
59            "prelude/internal/syntax",
60            include_str!("../../prelude/internal/syntax.rslisp"),
61        ),
62        (
63            "prelude/syntax",
64            include_str!("../../prelude/syntax.rslisp"),
65        ),
66        (
67            "prelude/number",
68            include_str!("../../prelude/number.rslisp"),
69        ),
70        ("prelude/text", include_str!("../../prelude/text.rslisp")),
71        (
72            "prelude/struct",
73            include_str!("../../prelude/struct.rslisp"),
74        ),
75        (
76            "prelude/definers",
77            include_str!("../../prelude/definers.rslisp"),
78        ),
79        (
80            "prelude/ownership",
81            include_str!("../../prelude/ownership.rslisp"),
82        ),
83        ("prelude/types", include_str!("../../prelude/types.rslisp")),
84        ("prelude/pair", include_str!("../../prelude/pair.rslisp")),
85        ("prelude/assoc", include_str!("../../prelude/assoc.rslisp")),
86        ("prelude/place", include_str!("../../prelude/place.rslisp")),
87        ("prelude/vec", include_str!("../../prelude/vec.rslisp")),
88        (
89            "prelude/iterator",
90            include_str!("../../prelude/iterator.rslisp"),
91        ),
92        (
93            "prelude/option",
94            include_str!("../../prelude/option.rslisp"),
95        ),
96        (
97            "prelude/result",
98            include_str!("../../prelude/result.rslisp"),
99        ),
100        ("prelude/io", include_str!("../../prelude/io.rslisp")),
101        ("prelude/map", include_str!("../../prelude/map.rslisp")),
102        (
103            "prelude/sequence",
104            include_str!("../../prelude/sequence.rslisp"),
105        ),
106        ("prelude/test", include_str!("../../prelude/test.rslisp")),
107        (
108            "prelude/project-file",
109            include_str!("../../prelude/project-file.rslisp"),
110        ),
111    ];
112
113    pub fn new(root: PathBuf, display_root: PathBuf) -> Self {
114        Self {
115            root,
116            display_root,
117            fallback_root: None,
118            libraries: LibraryTable::default(),
119        }
120    }
121
122    pub fn with_fallback_root(self, fallback_root: PathBuf) -> Self {
123        Self {
124            fallback_root: Some(fallback_root),
125            ..self
126        }
127    }
128
129    pub fn with_libraries(self, libraries: LibraryTable) -> Self {
130        Self { libraries, ..self }
131    }
132
133    pub fn resolve(&self, owner: Option<&str>, written: &ModulePath) -> ModulePath {
134        if !Self::is_written(written) {
135            return written.clone();
136        }
137        self.libraries.resolve(owner, written)
138    }
139
140    pub fn library_owner<'p>(&self, path: &'p ModulePath) -> Option<&'p str> {
141        self.libraries
142            .owner_of(path)
143            .map(|_| LibraryTable::head(path))
144    }
145
146    pub fn unlisted_library(&self, owner: Option<&str>, written: &ModulePath) -> Option<&str> {
147        self.libraries
148            .unlisted_library(owner, written)
149            .map(LibraryRoot::name)
150    }
151
152    pub fn local_module_beside_library(
153        &self,
154        owner: Option<&str>,
155        written: &ModulePath,
156    ) -> Option<LocalModuleClash<'_>> {
157        let library = self.libraries.unlisted_library(owner, written)?;
158        Some(LocalModuleClash {
159            local: self.local_module(written)?,
160            library: library.name(),
161            lister: library.lister()?,
162        })
163    }
164
165    pub fn hidden_local_module(
166        &self,
167        owner: Option<&str>,
168        written: &ModulePath,
169    ) -> Option<PathBuf> {
170        if owner.is_some() || !self.libraries.names_library(owner, written) {
171            return None;
172        }
173        self.local_module(written)
174    }
175
176    pub fn hidden_source_module(
177        &self,
178        owner: Option<&str>,
179        written: &ModulePath,
180    ) -> Option<HiddenSourceModule> {
181        let fallback_root = self.fallback_root.as_ref()?;
182        if owner.is_some() || self.libraries.owner_of(written).is_some() {
183            return None;
184        }
185        let file_name = Self::file_name(written);
186        let hidden = fallback_root.join(&file_name);
187        let hides = self.root.join(&file_name).is_file() && hidden.is_file();
188        hides.then(|| HiddenSourceModule {
189            hiding: self.display_root.join(&file_name),
190            hidden,
191        })
192    }
193
194    pub fn is_written_file(name: &str) -> bool {
195        Path::new(name).is_file()
196    }
197
198    fn local_module(&self, written: &ModulePath) -> Option<PathBuf> {
199        let head = LibraryTable::head(written);
200        let file = PathBuf::from(format!("{head}.{}", Language::EXTENSION));
201        self.local_roots().find_map(|(root, display_root)| {
202            [&file, Path::new(head)]
203                .into_iter()
204                .find(|local| root.join(local).exists())
205                .map(|local| display_root.join(local))
206        })
207    }
208
209    fn local_roots(&self) -> impl Iterator<Item = (&Path, &Path)> {
210        let fallback = self.fallback_root.as_deref();
211        std::iter::once((self.root.as_path(), self.display_root.as_path()))
212            .chain(fallback.map(|root| (root, root)))
213    }
214
215    pub fn is_prelude(path: &ModulePath) -> bool {
216        path.as_str() == Self::PRELUDE || path.as_str().starts_with(Self::PRELUDE_DIRECTORY)
217    }
218
219    pub fn is_internal(path: &ModulePath) -> bool {
220        path.as_str().starts_with(Self::INTERNAL_DIRECTORY) && Self::is_embedded_prelude(path)
221    }
222
223    pub fn is_embedded_prelude(path: &ModulePath) -> bool {
224        Self::PRELUDE_FILES
225            .iter()
226            .any(|(module, _)| *module == path.as_str())
227    }
228
229    pub fn is_written(path: &ModulePath) -> bool {
230        !Self::is_prelude(path) && !Self::is_compiler_provided(path)
231    }
232
233    pub fn is_reserved_library(name: &str) -> bool {
234        [Self::PRELUDE, Self::KERNEL, Self::CODE].contains(&name)
235    }
236
237    pub fn is_reserved(path: &ModulePath) -> bool {
238        path.as_str() == Language::PROJECT_FILE_STEM
239    }
240
241    pub fn is_compiler_provided(path: &ModulePath) -> bool {
242        [Self::KERNEL, Self::CODE].contains(&path.as_str())
243    }
244
245    pub fn relative_name<'n>(&self, name: &'n str) -> &'n str {
246        Path::new(name)
247            .strip_prefix(&self.display_root)
248            .ok()
249            .and_then(Path::to_str)
250            .unwrap_or(name)
251    }
252
253    pub fn read_file(path: &Path) -> io::Result<String> {
254        fs::read_to_string(path).map(Self::without_byte_order_mark)
255    }
256
257    pub fn read_standard_input() -> io::Result<String> {
258        io::read_to_string(io::stdin()).map(Self::without_byte_order_mark)
259    }
260
261    fn without_byte_order_mark(mut text: String) -> String {
262        if text.starts_with(Self::BYTE_ORDER_MARK) {
263            text.drain(..Self::BYTE_ORDER_MARK.len_utf8());
264        }
265        text
266    }
267
268    pub fn find(&self, path: &ModulePath) -> Option<ModuleSource> {
269        self.builtin(path)
270            .or_else(|| self.prelude_file(path))
271            .or_else(|| self.written_file(path))
272    }
273
274    fn builtin(&self, path: &ModulePath) -> Option<ModuleSource> {
275        match path.as_str() {
276            Self::KERNEL => Some(ModuleSource::Builtin(BuiltinModule::Kernel)),
277            Self::CODE => Some(ModuleSource::Builtin(BuiltinModule::Code)),
278            _ => None,
279        }
280    }
281
282    fn prelude_file(&self, path: &ModulePath) -> Option<ModuleSource> {
283        Self::PRELUDE_FILES
284            .iter()
285            .find(|(module, _)| *module == path.as_str())
286            .map(|(_, text)| ModuleSource::Text {
287                name: Self::file_name(path),
288                text: (*text).to_string(),
289            })
290    }
291
292    fn written_file(&self, path: &ModulePath) -> Option<ModuleSource> {
293        match self.libraries.owner_of(path) {
294            Some(_) => self.library_file(path),
295            None => self.user_file(path),
296        }
297    }
298
299    pub fn module_outside_checkout(&self, path: &ModulePath) -> Option<PathBuf> {
300        let checkout = self.libraries.checkout_of(path)?;
301        let file = self.libraries.file(path)?;
302        let resolved = fs::canonicalize(&file.path).ok()?;
303        (!resolved.starts_with(checkout)).then_some(file.display)
304    }
305
306    fn library_file(&self, path: &ModulePath) -> Option<ModuleSource> {
307        let file = self.libraries.file(path)?;
308        let text = Self::read_file(&file.path).ok()?;
309        Some(ModuleSource::Text {
310            name: file.display.display().to_string(),
311            text,
312        })
313    }
314
315    fn user_file(&self, path: &ModulePath) -> Option<ModuleSource> {
316        let file_name = Self::file_name(path);
317        self.local_roots().find_map(|(root, display_root)| {
318            let text = Self::read_file(&root.join(&file_name)).ok()?;
319            Some(ModuleSource::Text {
320                name: display_root.join(&file_name).display().to_string(),
321                text,
322            })
323        })
324    }
325
326    fn file_name(path: &ModulePath) -> String {
327        format!("{path}.{}", Language::EXTENSION)
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use std::path::PathBuf;
334
335    use super::ModuleSources;
336    use crate::core::ModulePath;
337    use crate::modules::libraries::{LibraryRoot, LibraryTable};
338
339    struct TestedApp {
340        directory: PathBuf,
341    }
342
343    impl TestedApp {
344        const LIBRARY: &str = "greeting";
345
346        fn new() -> Self {
347            Self {
348                directory: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
349                    .join("../tests/fixtures/libraries/tested-app"),
350            }
351        }
352
353        fn sources(&self) -> ModuleSources {
354            let tests = self.directory.join("shadowing-tests");
355            ModuleSources::new(tests.clone(), tests).with_fallback_root(self.directory.join("src"))
356        }
357
358        fn sources_with_library(&self) -> ModuleSources {
359            let library = LibraryRoot::new(
360                Self::LIBRARY.to_string(),
361                self.directory.join("src"),
362                self.directory.join("src"),
363                "main".to_string(),
364                Vec::new(),
365            );
366            let table = LibraryTable::new(vec![Self::LIBRARY.to_string()], vec![library]);
367            self.sources().with_libraries(table)
368        }
369
370        fn hidden(
371            &self,
372            sources: &ModuleSources,
373            owner: Option<&str>,
374            written: &str,
375        ) -> Option<PathBuf> {
376            sources
377                .hidden_source_module(owner, &ModulePath::new(written.to_string()))
378                .map(|shadow| shadow.hidden)
379        }
380    }
381
382    #[test]
383    fn a_test_module_hides_the_source_module_of_its_path() {
384        let app = TestedApp::new();
385        let hidden = app.hidden(&app.sources(), None, "greeting");
386        assert_eq!(hidden, Some(app.directory.join("src/greeting.rslisp")));
387    }
388
389    #[test]
390    fn a_source_module_without_a_test_module_is_not_hidden() {
391        let app = TestedApp::new();
392        assert_eq!(app.hidden(&app.sources(), None, "parser"), None);
393    }
394
395    #[test]
396    fn a_test_module_without_a_source_module_hides_nothing() {
397        let app = TestedApp::new();
398        assert_eq!(app.hidden(&app.sources(), None, "greets"), None);
399    }
400
401    #[test]
402    fn a_use_inside_a_library_hides_nothing() {
403        let app = TestedApp::new();
404        assert_eq!(app.hidden(&app.sources(), Some("hints"), "greeting"), None);
405    }
406
407    #[test]
408    fn a_library_path_hides_nothing() {
409        let app = TestedApp::new();
410        assert_eq!(
411            app.hidden(&app.sources_with_library(), None, "greeting"),
412            None
413        );
414    }
415}