Skip to main content

knave_expand/modules/
sources.rs

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