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}