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}