Skip to main content

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}