zelkova_compiler/source/mod.rs
1pub mod files;
2pub mod overlay;
3
4pub use files::{SourceFile, SourceFiles, SourceRoot};
5pub use overlay::Overlay;
6
7use super::{CompilationError, PackageName};
8use files::SourceFileError;
9use std::collections::HashSet;
10use std::path::Path;
11use walkdir::WalkDir;
12
13/// Walk one of a package's source roots into a database of its own.
14///
15/// Nothing else can be in that database, so a file is named by its place in the
16/// package and nothing more — see [`load_package_sources_into`] for the build case,
17/// where that name is not unique.
18// We don't support non-UTF8 characters in path
19pub fn load_package_sources(
20 package_dir: &Path,
21 root: SourceRoot,
22) -> Result<SourceFiles, CompilationError> {
23 let mut sources = SourceFiles::new();
24 load_package_sources_into(package_dir, root, None, &Overlay::new(), &mut sources)?;
25 Ok(sources)
26}
27
28/// Walk one source root of the package rooted at `package_dir` for `.zel` files, add
29/// each one to `files`, and return the id each was given.
30///
31/// A package has two source roots and they are walked separately, because a module's
32/// name is its path under *its own* root: `src/Model.zel` and `tests/Model.zel` are
33/// both `Model` ([*Source
34/// roots*](../../docs/spec/packages.md#source-roots)). Each file carries the root
35/// it came from, so a diagnostic can tell the two apart.
36///
37/// A package must have a `src/`, and a walk of one that is not there is the error
38/// `WalkDir` reports. A `tests/` that is not there is a package that holds no tests,
39/// which is not an error and loads as no modules at all.
40///
41/// A build holds one file database for every package in it, because a
42/// [`SourceFileId`](files::SourceFileId) travels inside an `Interface` and outlives the
43/// package it came from (`ERR-5`), so the ids of two packages have to be drawn from one
44/// sequence. This is the entry point that appends into that shared database;
45/// [`load_package_sources`] is the same walk over a database of its own, which is what
46/// a caller compiling exactly one source root wants.
47///
48/// `package` names the package `package_dir` holds, and every caller sharing a database
49/// across packages has to pass it: a path relative to a package's own `src/` is not
50/// unique in a build — two packages may each hold a `Size.zel` — so without it a
51/// diagnostic cannot say which package it is about.
52///
53/// `overlay` holds the text of buffers that are open in an editor: a file it holds is
54/// read from there instead of from disk, and a `.zel` file under the root that only the
55/// overlay holds is loaded after the walked ones, in path order. [`Overlay`] has the
56/// rules.
57///
58/// The ids come back rather than the files themselves: `files` is borrowed mutably for
59/// the walk, and the caller needs it borrowed immutably afterwards to read the sources
60/// back out.
61pub fn load_package_sources_into(
62 package_dir: &Path,
63 root: SourceRoot,
64 package: Option<&PackageName>,
65 overlay: &Overlay,
66 sources: &mut SourceFiles,
67) -> Result<Vec<files::SourceFileId>, CompilationError> {
68 let mut loaded = vec![];
69 let mut errors = vec![];
70
71 let root_dir = package_dir.join(root.directory());
72
73 // A package that holds no tests has no `tests/` to walk, and that is not a failure
74 // the way a missing `src/` is. Asking first is what keeps the walk's own
75 // does-not-exist error meaning what it says everywhere else. It skips the walk and
76 // nothing more: a buffer under a `tests/` that is not on disk is still loaded below.
77 let walk = !(root == SourceRoot::Tests && !root_dir.exists());
78
79 // What the walk matched, normalised, so that an overlay key naming the same file is
80 // not loaded a second time.
81 let mut walked: HashSet<std::path::PathBuf> = HashSet::new();
82
83 // `WalkDir`'s iterator advances past every entry it yields — including an `Err`
84 // one — and terminates on its own (a root that doesn't exist yields exactly one
85 // `Err` and then ends), so this loop cannot spin on a single failing entry the
86 // way the invariant in `CLAUDE.md` about `Result`-yielding iterators warns
87 // against.
88 if walk {
89 for entry in WalkDir::new(&root_dir).follow_links(true) {
90 match entry {
91 Ok(entry) => {
92 let path = entry.path().to_path_buf();
93
94 match path.extension() {
95 Some(ext) if ext == "zel" => {}
96 _ => continue,
97 }
98
99 walked.insert(overlay::normalise(&path));
100
101 match SourceFile::load(path, &root_dir, root, package, overlay) {
102 Ok(src) => {
103 loaded.push(sources.add_file(src));
104 }
105 Err(err) => {
106 errors.push(err);
107 }
108 }
109 }
110 Err(walk_err) => {
111 errors.push(SourceFileError::from_walk_error(&root_dir, walk_err));
112 }
113 }
114 }
115 }
116
117 // What only the overlay holds. The root is normalised the way the keys are, and a
118 // file is named by its path under it.
119 let normalised_root = overlay::normalise(&root_dir);
120 for path in overlay.zel_files_under(&normalised_root) {
121 if walked.contains(path) {
122 continue;
123 }
124 match SourceFile::load(path.to_path_buf(), &normalised_root, root, package, overlay) {
125 Ok(src) => {
126 loaded.push(sources.add_file(src));
127 }
128 Err(err) => {
129 errors.push(err);
130 }
131 }
132 }
133
134 if errors.is_empty() {
135 Ok(loaded)
136 } else {
137 Err(errors.into())
138 }
139}