Skip to main content

zelkova_compiler/
lib.rs

1//! The Zelkova compiler
2//!
3//!
4//!# How to compile a package ?
5//!
6//! Note: an interface is built for every module that canonicalizes and kept for whatever
7//! imports it within its package; a package that checks also offers its modules'
8//! interfaces to the packages that depend on it. What is still missing is emitting one,
9//! so a dependency is compiled from source on every build.
10//!
11//! 1. Read the package's `zelkova.toml` manifest, and resolve the build: every package
12//!    reachable through `dependencies`, plus the root package's `test-dependencies`, each
13//!    ordered after the packages it depends on. Steps 2 to 6 then run once per package —
14//!    bar the ones only `test-dependencies` reaches, which a build that did not ask for
15//!    the tests resolves and leaves uncompiled — over the two source roots: `src/` and,
16//!    for the root package when its tests were asked for, `tests/`, beside its own
17//!    manifest. The root's `tests/` is compiled last, after the packages only
18//!    `test-dependencies` reach, since one of those may depend on the root's `src/`.
19//!    Before a package's modules are checked, the whole map of module names it can
20//!    import is built — its own, plus each
21//!    direct dependency's public modules under that package's namespace or, unwrapped,
22//!    under their own names — and a name two modules both answer to stops that package
23//!    there.
24//! 2. Collect all `*.zelkova` files with their path name relatives to the root.
25//! 3. Create a `SourceFiles` mapping from `ModuleName` to `parser::Module`.
26//!     1. module names are deduced from file name
27//!     2. parsing is done through `parser::parse_recovering`, which reports every
28//!        declaration that fails rather than only the first. A file whose header parsed
29//!        keeps its module, with a record of each declaration that failed; one whose
30//!        header did not is still a module its importers find, holding nothing
31//!     3. Verify that `parser::Module.name` match the one from the file system
32//! 4. Build a dependency graphs from the modules import
33//!     1. build it
34//!     2. Verify there is no cyclic relation between modules
35//! 5. Following the deps graph,
36//!     1. canonicalize each modules. A declaration that does not canonicalize costs the
37//!        module that declaration and nothing else, an import that does not resolve costs
38//!        it that import and nothing else, and the module publishes its `Interface` to
39//!        the modules checked after it, whatever the checks find in it. A not-found
40//!        error that only restates one of those failures is not reported again
41//!        (`canonical::canonicalize_recovering`)
42//!     1. check each module (type check, exhaustiveness, etc…), and build the `ir::Module`
43//!        a backend reads out of the canonical module and what the typer solved. The IR
44//!        is built whether or not a declaration was broken or rejected, and lists each
45//!        such one as unchecked
46//!     1. Bonus point to parallelize the tree branches which are not dependent on each others
47//! 6. Report. Each phase returns every error it found, `check_module_recovering` tags those with
48//!    the module they came from, and `check_package` accumulates them across modules and
49//!    hands them back with the file database and every status line, printing nothing.
50//!    See the `PhaseError` trait for what a phase error owes the `Diagnostic` it renders
51//!    as.
52//!
53//! Every step is `check_package`'s, which writes nothing anywhere. Emitting a build that
54//! checked, writing it and printing what the check found are the `driver` module's.
55//!
56
57use codespan_reporting::diagnostic::{Diagnostic, Label};
58use log::debug;
59use std::collections::{HashMap, HashSet};
60use std::path::{Path, PathBuf};
61
62pub mod canonical;
63// Public because it is a rule about the language rather than an implementation detail of
64// one phase, and because two phases read it: canonicalization synthesises the imports, and
65// `dependencies` puts the matching edges in the import graph.
66pub mod default_imports;
67// Public so that `crates/zelkova/tests/pipeline.rs` can drive `ModuleWalker::check_in_order` with the
68// real `check_module_recovering`, which is the only seam that observes the modules that checked
69// successfully alongside the ones that failed (`BUG-2`) — `compile_package` only reports
70// them to stderr. `dependencies::Error` was already reachable from the public
71// `CompilationError::DependenciesError`, so this names an existing part of the API
72// rather than widening it.
73pub mod dependencies;
74// Public for the same reason as `dependencies`: `exhaustiveness::Error` is
75// reachable from the public `CompilationError::Exhaustiveness`, so the module
76// that defines it has to be nameable. It also puts the last phase module on the
77// same footing as `canonical`, `typer` and `parser`.
78pub mod exhaustiveness;
79// Public because it is the compiler's hand-off to a backend and `check_module` returns one.
80pub mod ir;
81// Public for the same reason as `source` and `dependencies` — `manifest::ManifestError` is
82// reachable from the public `CompilationError::Manifest`.
83pub mod manifest;
84pub mod name;
85// Public for the same reason as `manifest`: `program::Error` is reachable from the public
86// `CompilationError::Program`.
87pub mod program;
88// Public for the same reason as `manifest`: `resolve::Error` is reachable from the public
89// `CompilationError::Resolution`.
90pub mod resolve;
91// Public for the same reason as `default_imports`: it is a rule about the language rather
92// than an implementation detail of one phase.
93pub mod scalars;
94pub mod source;
95pub mod typer;
96mod utils;
97
98use name::{Name, QualName};
99use source::files::{SourceFileError, SourceFileId};
100use source::{Overlay, SourceFiles};
101use zelkova_syntax::parser;
102use zelkova_syntax::position::{BytePos, NodeSpan, Span};
103
104// TODO Move PackageName and ModuleName into the name module
105/// A package name: one flat identifier, ASCII lowercase letters, digits and hyphens,
106/// starting with a letter, with every hyphen followed by a letter —
107/// [`docs/spec/packages.md`](../docs/spec/packages.md#the-manifest)'s whole rule for
108/// `name`. That shape is what keeps a package name and a module name from ever being
109/// confused (one is lowercase-with-hyphens, the other uppercase-with-dots), and what makes
110/// the namespace a package's name derives unambiguous.
111///
112/// The only way to build one is [`PackageName::new`], which rejects anything that does not
113/// match the rule — there is no way to hold a `PackageName` that manifest validation would
114/// have refused.
115#[derive(Eq, PartialEq, Hash, Debug, Clone)]
116pub struct PackageName(String);
117
118/// `PackageName::new` refused this string; it names the input so a caller can report it.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct InvalidPackageName(pub String);
121
122impl PackageName {
123    /// Build a `PackageName`, or say why `name` is not one.
124    pub fn new<S: Into<String>>(name: S) -> Result<PackageName, InvalidPackageName> {
125        let name = name.into();
126        if is_legal_package_name(&name) {
127            Ok(PackageName(name))
128        } else {
129            Err(InvalidPackageName(name))
130        }
131    }
132
133    pub fn as_str(&self) -> &str {
134        &self.0
135    }
136
137    /// `zelkova-core`, [`resolve::CORE_PACKAGE`]: the one package the compiler names
138    /// on its own, because it is where the [scalars] are declared.
139    ///
140    /// Built without going through [`PackageName::new`]'s check, so that naming a scalar
141    /// needs no `unwrap`; that the constant passes the check is a unit test.
142    pub fn core() -> PackageName {
143        PackageName(resolve::CORE_PACKAGE.to_string())
144    }
145
146    /// Whether this is `zelkova-core`, the one package the default imports and the
147    /// scalar seeding they are replaced by both key themselves on
148    /// ([`DEC-17`](../docs/decisions/dec-17.md) decision 4) — and, since
149    /// [`resolve::visible_modules`] rejects any other package that declares one of
150    /// the eight, the only package that can be shaped like it.
151    pub fn is_core(&self) -> bool {
152        self.0 == resolve::CORE_PACKAGE
153    }
154
155    /// The prefix this package's modules are named through from outside it: the name
156    /// split at its hyphens, each piece capitalised, joined —
157    /// [*The namespace*](../docs/spec/packages.md#the-namespace). `acme-widgets` is
158    /// `AcmeWidgets`, `todo` is `Todo`.
159    ///
160    /// It is a [`Name`] and not a [`PackageName`]: what comes back is a module-name
161    /// prefix, and the whole point of the shape rule above is that the two are never
162    /// confusable. Distinct package names give distinct namespaces, which is what lets
163    /// two wrapped dependencies keep out of each other's way with nothing checked.
164    ///
165    /// A package never writes its own namespace, so nothing inside the package being
166    /// compiled goes through here: [`resolve::visible_modules`] is the one caller, and
167    /// it applies this only to a *dependency*'s modules.
168    pub fn namespace(&self) -> Name {
169        let mut namespace = String::with_capacity(self.0.len());
170
171        for piece in self.0.split('-') {
172            let mut characters = piece.chars();
173            if let Some(first) = characters.next() {
174                namespace.extend(first.to_uppercase());
175                namespace.push_str(characters.as_str());
176            }
177        }
178
179        Name::new(namespace)
180    }
181}
182
183impl std::fmt::Display for PackageName {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        write!(f, "{}", self.0)
186    }
187}
188
189/// ASCII lowercase letters, digits and hyphens; starts with a letter; every hyphen is
190/// immediately followed by a letter (so no leading, trailing or doubled hyphen, and no
191/// hyphen before a digit).
192fn is_legal_package_name(name: &str) -> bool {
193    match name.chars().next() {
194        Some(c) if c.is_ascii_lowercase() => {}
195        _ => return false,
196    }
197
198    for (i, c) in name.char_indices() {
199        if c == '-' {
200            match name[i + 1..].chars().next() {
201                Some(next) if next.is_ascii_lowercase() => {}
202                _ => return false,
203            }
204        } else if !(c.is_ascii_lowercase() || c.is_ascii_digit()) {
205            return false;
206        }
207    }
208
209    true
210}
211
212/// A module name represent
213#[derive(Eq, PartialEq, Hash, Debug, Clone)]
214pub struct ModuleName {
215    package: PackageName,
216    name: Name, // including dots
217}
218
219impl ModuleName {
220    pub fn new(package: PackageName, name: Name) -> ModuleName {
221        ModuleName { package, name }
222    }
223
224    pub fn name(&self) -> &Name {
225        &self.name
226    }
227
228    /// The package that declares this module.
229    pub fn package(&self) -> &PackageName {
230        &self.package
231    }
232
233    /// The name `name`, as declared by this module of this package.
234    pub fn qualify_name(&self, name: &Name) -> QualName {
235        QualName::in_module(self.package.clone(), self.name.as_str(), name.as_str())
236    }
237
238    fn as_human_string(&self) -> String {
239        format!("{}:{}", self.package, self.name)
240    }
241}
242
243/// A byte span paired with the file it was written in.
244///
245/// [`NodeSpan`] is enough for an error about the module a phase is currently
246/// checking, because [`check_package`] supplies that one file id for the whole
247/// diagnostic at render time (see [`PhaseError`] and [`SpanLabel`]). A span pulled
248/// out of an [`Interface`] cannot rely on that: it was written in the *exporting*
249/// module's file, which is not necessarily the file being rendered, so the id has
250/// to travel with the span instead of being attached later. This is that pair —
251/// [`Interface::source_span`] is how one gets built.
252#[derive(Debug, Clone, Copy, PartialEq)]
253pub struct SourceSpan {
254    pub file: SourceFileId,
255    pub span: Span<BytePos>,
256}
257
258/// An interface is trim down version of a module.
259///
260/// We use it when translating a local source AST into its canonical form as
261/// an optimization technique. Instead of parsing every source files on each
262/// file compilation, we save the publicly exposed information of a successfully
263/// parsed module and only load this information on module depending on it.
264///
265/// All `Interface` indices are using non-qualified names. To get the qualified
266/// version, simply use `I.module_name.qualify_name(&name)`.
267//
268// `Clone` because one interface reaches more than one place: a public module of a
269// dependency is inserted into the environment of every package that imports it, under
270// whichever spelling that package names it by (`resolve::visible_modules`). The
271// interface itself is the same either way — a spelling is the importer's, and what an
272// `Interface` carries is the module's own name.
273#[derive(Debug, Clone)]
274pub struct Interface {
275    pub module_name: ModuleName,
276    /// Each value's type and the context its annotation wrote, with where its
277    /// declaration was written: a [`canonical::ValueSignature`].
278    pub values: HashMap<Name, canonical::ValueSignature>,
279    /// Every union type the module exposes, opaquely or not. An opaque one — see
280    /// [`opaque_unions`](Self::opaque_unions) — is here with its `variants` emptied.
281    pub unions: HashMap<Name, canonical::UnionType>,
282    /// The names in [`unions`](Self::unions) that the module exposes *opaquely*: a
283    /// bare `Size` in its `exposing` header rather than `Size(..)`.
284    ///
285    /// An opaque entry's `variants` is empty, and so is that of a type declared with
286    /// no constructors at all (`type Empty =`, which the grammar accepts today and
287    /// [`LANG-10`](../../docs/tickets/lang-10.md) is to reject), so
288    /// `variants` alone cannot say whether the constructors were withheld. This set
289    /// is what says it. `canonical::environment::process_import` reads it to reject a
290    /// `Size(..)` import entry for such a type, and
291    /// [`default_imports`] to leave out a default import
292    /// asking for constructors the module does not expose.
293    ///
294    /// [`canonical::Module::to_interface`] fills it. A hand-built interface that leaves
295    /// it empty exposes every union it holds with whatever `variants` it carries.
296    pub opaque_unions: HashSet<Name>,
297    // TODO type aliases
298    //aliases: HashMap<Name, >
299    /// infixes is a map from the operator symbol to its information
300    pub infixes: HashMap<Name, canonical::Infix>,
301    /// The signature of an exposed infix's own backing function, keyed by that
302    /// function's unqualified name — populated only when the function is *not*
303    /// separately present in [`values`](Self::values), i.e. the header exposes
304    /// the operator but not the function by name (`infix left 6 (+) = add`,
305    /// header exposing `(+)` and not `add`, is `std/core`'s own shape for every
306    /// operator it declares).
307    ///
308    /// `canonical::environment::imported_infix` is what needs it: an operator is
309    /// resolved through its `infix` declaration, and the declaration alone names
310    /// the backing function without saying anything about its type. Whichever of
311    /// the two maps holds it, that is where the type comes from — and for an
312    /// operator whose function is not separately exposed, this is the only one
313    /// that can.
314    ///
315    /// Nothing else reads it, and in particular nothing inserts it into an
316    /// importing module's scope under its own name: a function that backs an
317    /// exposed operator is importable by name only when the header says so too
318    /// (`BUG-9`), and then it is [`values`](Self::values) that carries it.
319    pub infix_functions: HashMap<Name, canonical::ValueSignature>,
320    /// How many parameters each value in [`values`](Self::values) and
321    /// [`infix_functions`](Self::infix_functions) is emitted with, keyed the same way:
322    /// [`canonical::Module::emitted_arity`].
323    ///
324    /// An importer reads it for the same reason the module declaring the value reads its
325    /// own declarations' arities: a call supplying that many arguments is a direct call,
326    /// and a partial application, or a use as a value of one taking two or more, goes
327    /// through the runtime's `$curry`
328    /// ([`DEC-18` decision
329    /// 3](../docs/decisions/dec-18.md#3--a-function-emits-as-a-plain-n-ary-function-and-currying-is-a-runtime-helper)).
330    /// The typer's translation is where it is read, into [`ir::ReferenceKind::Foreign`].
331    ///
332    /// [`canonical::Module::to_interface`] records one for every value either map holds,
333    /// except a value its module recorded in [`canonical::Module::broken`], whose
334    /// parameters are unknown. A value missing from it — a broken one, or one a hand-built
335    /// interface in a test leaves out — is read as arity 0, the arity of a parameterless
336    /// binding.
337    pub arities: HashMap<Name, usize>,
338    /// Every class the module declares and exposes, by name, with its members and the
339    /// derivations it carries, which are what an importer writes a `derived` instance's
340    /// members out of. A member is not in [`values`](Self::values): an importer reaches it
341    /// through its class.
342    pub classes: HashMap<Name, canonical::ClassSignature>,
343    /// Every instance in scope in the module: the ones it declares, and every one an
344    /// import brought into it.
345    ///
346    /// The one part of an interface the module's `exposing` header does not filter. An
347    /// instance has no name an importer could write, and it is in scope wherever its
348    /// class and its type are, so it travels through every import, transitively, whether
349    /// or not the importer named anything ([Type
350    /// classes](../../docs/spec/type-classes.md#declaring-an-instance)). Each entry
351    /// records the module that declared it, which is what recognises the same instance
352    /// arriving by two routes as one.
353    pub instances: Vec<canonical::PublishedInstance>,
354    /// The file this interface's module was read from, when the caller knows it.
355    ///
356    /// It is what makes a diagnostic about an imported name able to underline that
357    /// name's *own* source rather than merely naming its module. Nothing inside a
358    /// module check can supply it: a phase only ever sees one module and never
359    /// learns its [`SourceFileId`] (see [`PhaseError`]). Driver code does, so
360    /// [`canonical::Module::to_interface`] takes it as an argument, and its one
361    /// caller — [`dependencies::ModuleWalker::check_in_order`], which like
362    /// [`check_package`] is a driver loop and already holds the file each module
363    /// came from — passes it there. A hand-built interface, as every test below
364    /// `check_module` constructs, passes `None`; [`Interface::source_span`] then has
365    /// no file to pair a span with and returns `None`, so the diagnostic degrades to
366    /// no secondary label rather than a wrong one.
367    pub file: Option<SourceFileId>,
368    /// A name looked up in this interface and not found may be one its module declares
369    /// and could not publish.
370    ///
371    /// [`canonical::Module::to_interface`] copies the module's own
372    /// [`incomplete`](canonical::Module::incomplete), which is set when the module's
373    /// scope was incomplete or when a declaration's annotation did not canonicalize or
374    /// may be a chunk that failed to parse. [`Interface::unavailable`], the stand-in for a
375    /// module whose header did not parse, sets it. An importer that names something this
376    /// interface lacks reports nothing about it, because the failure that made the name
377    /// missing has already been reported against the module ([`DEC-23` decision
378    /// 3](../docs/decisions/dec-23.md#3--an-error-that-restates-a-reported-failure-is-dropped-by-a-flag-on-the-scope)),
379    /// and the importer's own scope is incomplete in turn. A hand-built interface
380    /// leaves it `false`.
381    pub incomplete: bool,
382}
383
384impl Interface {
385    /// The interface of a module whose header did not parse: it holds nothing, and is
386    /// [incomplete](Self::incomplete), so an importer finds the module and reports
387    /// nothing about what it does not find in it. `file` is the file the module was read
388    /// from.
389    ///
390    /// The header's syntax error is the one that stands behind every name missing from
391    /// it, and is reported with the package, which therefore publishes nothing: the
392    /// stand-in is only ever seen by the modules of its own package.
393    pub fn unavailable(module_name: ModuleName, file: SourceFileId) -> Interface {
394        Interface {
395            module_name,
396            values: HashMap::new(),
397            unions: HashMap::new(),
398            opaque_unions: HashSet::new(),
399            infixes: HashMap::new(),
400            infix_functions: HashMap::new(),
401            arities: HashMap::new(),
402            classes: HashMap::new(),
403            instances: Vec::new(),
404            file: Some(file),
405            incomplete: true,
406        }
407    }
408
409    /// Pair a [`NodeSpan`] taken from this interface's own data — a value's
410    /// declaration, a [`canonical::UnionType`]'s or [`canonical::Infix`]'s `span`
411    /// field — with the file it was written in, when both halves are known.
412    ///
413    /// Both halves are needed: a `NodeSpan` with nothing behind it (a hand-built
414    /// test value) or an interface with no attached file (same) leaves nothing
415    /// honest to build, and `None` is that answer — the caller renders no label
416    /// rather than one pointing at byte 0 of the wrong file.
417    pub fn source_span(&self, span: NodeSpan) -> Option<SourceSpan> {
418        match (self.file, span.span()) {
419            (Some(file), Some(span)) => Some(SourceSpan { file, span }),
420            _ => None,
421        }
422    }
423}
424
425/// One underlined region of the user's source, and what to say about it.
426///
427/// # `file`: when a label is not about the module under check
428///
429/// A [`codespan_reporting::diagnostic::Label`] needs a file id as well as a byte range. Most
430/// labels don't carry one themselves: a phase only ever sees one module, so its
431/// spans belong to the file that module was read from, and [`check_package`] —
432/// the only place that knows which file that is — supplies it for the whole
433/// diagnostic at render time. `file: None` is that case, and it covers everything
434/// built before `ERR-5`.
435///
436/// `Some` is the escape hatch: a label built from a [`SourceSpan`] cloned out of an
437/// [`Interface`] (via [`Interface::source_span`]) already carries its own file,
438/// generally a *different* one from the module being checked — "the annotation you
439/// are contradicting is over there, in `Basics`" — and that id is used verbatim
440/// instead of the diagnostic's.
441///
442/// `primary` is the rustc distinction, and it applies independently of `file`: the
443/// caret under the thing that is wrong is primary even when it is local, and
444/// "defined here" in another file is secondary even though it is the only label
445/// pointing into that file.
446#[derive(Debug, Clone, PartialEq)]
447pub struct SpanLabel {
448    pub span: Span<BytePos>,
449    pub message: String,
450    pub primary: bool,
451    pub file: Option<SourceFileId>,
452}
453
454/// What a compiler phase owes the diagnostic reporter.
455///
456/// Each phase keeps its own error type — one enum for the whole compiler would make
457/// every phase depend on the vocabulary of every other. What they have to share is
458/// the ability to describe themselves in the user's terms, because
459/// [`CompilationError::as_diagnostic`] is the only place a `Diagnostic` is ever
460/// built and it has no phase-specific knowledge to fall back on. Dumping
461/// `format!("{:?}", e)` into a note is exactly what this trait replaces: a `Debug`
462/// dump names Rust types, not source constructs.
463///
464/// # How much a span can say, and where that stops
465///
466/// Every production in `grammar.lalrpop` that builds a node captures `@L`/`@R`, so
467/// declarations, expressions, patterns and types all know where they were written,
468/// and canonicalization copies that onto the node it builds. An error raised from one
469/// of those construction sites returns a [`SpanLabel`] from
470/// [`labels`](PhaseError::labels) and gets a caret under the text it is about — an
471/// unresolvable name is underlined at the identifier, not across the declaration
472/// containing it.
473///
474/// Not every error can. `labels` defaults to empty and that is a real answer, not a
475/// stub: an error raised while walking a node the grammar does not span — an
476/// `import`'s `exposing` list, say — has nowhere to point, and renders as message-plus-notes
477/// with no caret, exactly as every phase after parsing used to. An error that groups
478/// others (`canonical::Error::Many`, `EnvironmentErrors`) has no position of its own
479/// and flattens its members' labels instead, the way it already flattens their
480/// messages.
481///
482/// The typer is the phase that has to work for this, and it does: it does not check
483/// the canonical AST but a term language of its own, so it carries each canonical
484/// node's span into that language, each constraint records the term and the *reason*
485/// it came from, and `unify` reports the origin of the constraint it failed on. A
486/// type error therefore underlines the sub-expression that disagrees and adds a
487/// secondary label under the annotation that says what was expected (`ERR-4`).
488///
489/// `parser::Error` is still the one phase error that does not go through this trait
490/// at all — it builds its own labelled `Diagnostic` through `parser::Error::diagnostic`.
491///
492/// # The two ways a label learns its `SourceFileId`
493///
494/// A phase still never knows the id of the file the module it is checking came
495/// from — that half is unchanged, and stays attached by `compile_package`, the only
496/// place that knows it, the way `CompilationError::Source` already works. A
497/// [`SpanLabel`] with `file: None` — everything built before `ERR-5` — means
498/// exactly that: "in the module under check", resolved at render time.
499///
500/// That is right for an error entirely about one module and cannot cover a
501/// diagnostic that also names something written elsewhere — "the annotation you
502/// are contradicting is over there, in `Basics`". For that, `file` carries its own
503/// [`SourceFileId`] instead, taken from a [`SourceSpan`] cloned out of an
504/// [`Interface`] via [`Interface::source_span`]. An `Interface` can offer that
505/// because it is not a phase: it is built by driver code
506/// (`dependencies::ModuleWalker::check_in_order`) that already has the file the
507/// module it just checked came from, and stamps it onto the `Interface` before
508/// handing it to whoever imports next. `canonical::Type` still carries no span of
509/// its own — seeing why is worth a look at that type's own documentation.
510pub trait PhaseError {
511    /// One line naming what went wrong, in the vocabulary of the user's source.
512    ///
513    /// This is rendered as the diagnostic's headline, so it has to read on its own:
514    /// no `{:?}`, no Rust type names.
515    fn message(&self) -> String;
516
517    /// Supporting detail, one string per rendered note. Empty by default.
518    fn notes(&self) -> Vec<String> {
519        Vec::new()
520    }
521
522    /// The regions of the user's source this error is about, if it knows any.
523    ///
524    /// Empty — the default — means this error has no position to point at, and it
525    /// renders as message-plus-notes with no caret. See the trait's documentation.
526    fn labels(&self) -> Vec<SpanLabel> {
527        Vec::new()
528    }
529
530    /// This error's message followed by its notes.
531    ///
532    /// A `Diagnostic` has one headline, so an error that ends up inside a group —
533    /// several errors from one phase, or a variant like `canonical::Error::Many`
534    /// that wraps others — has to give up its headline and become notes. This is
535    /// that demotion, in one place, so a group cannot silently drop the message of
536    /// a member it swallowed.
537    fn message_and_notes(&self) -> Vec<String> {
538        std::iter::once(self.message())
539            .chain(self.notes())
540            .collect()
541    }
542}
543
544/// Turn `SpanLabel`s into the `codespan_reporting::Label`s a `Diagnostic` renders.
545///
546/// `fallback` is the file a label with none of its own resolves to — the module
547/// currently being checked, when the caller has one. So `fallback` is a fallback
548/// rather than a gate: a label needs *some* file, because a byte range on its own
549/// does not say which file to underline, but a [`SpanLabel`] carrying its own
550/// [`SpanLabel::file`] already has one and renders whether or not `fallback` is
551/// `Some`. Only a label with neither is a byte range nobody can place, and that one
552/// is dropped rather than guessed at.
553///
554/// This is the one place that distinction is applied; both [`phase_diagnostic`] (a
555/// fallback file, from the module under check) and the dependency-cycle arm of
556/// `CompilationError::as_diagnostic_in` (no fallback — a cycle has no single
557/// module) go through it.
558fn spans_to_labels(
559    labels: Vec<SpanLabel>,
560    fallback: Option<SourceFileId>,
561) -> Vec<Label<SourceFileId>> {
562    labels
563        .into_iter()
564        .filter_map(|l| {
565            // A label with its own file — a `SourceSpan` cloned out of an
566            // `Interface`, or (`ERR-6`) a `CycleEdge`'s file — points into that
567            // file instead of falling back. That is the whole mechanism `ERR-5`
568            // adds: everything built before it left `file` `None` and relied on
569            // `fallback` here.
570            let label_file = l.file.or(fallback)?;
571            let range = l.span.to_range();
572            let label = if l.primary {
573                Label::primary(label_file, range)
574            } else {
575                Label::secondary(label_file, range)
576            };
577            Some(label.with_message(l.message))
578        })
579        .collect()
580}
581
582/// Render the errors one phase produced for one module.
583///
584/// A `Diagnostic` has room for exactly one headline, so a lone error gets to be that
585/// headline and a group is summarised instead, with every message demoted to a note.
586/// `phase` names the phase in that summary line ("canonical", "type", …).
587///
588/// `file` is the module's source file when the caller knows it, and it is handed
589/// straight to `spans_to_labels` as the fallback that function describes: a label
590/// of its own — the cross-module ones `ERR-5` added — renders regardless, so a
591/// `CompilationError` built by hand, as the tests in this module do, loses only the
592/// labels about the module under check. [`check_package`] always wraps in
593/// [`CompilationError::InFile`] and so loses none.
594///
595/// Both branches attach labels: only the headline demotion differs between one error
596/// and a group, and an error that got swallowed into a note still knows where it was.
597///
598/// Public because [`CompilationError::as_diagnostic`] shares it with
599/// `zelkova::BuildError::as_diagnostic`, which renders the driver's own per-module
600/// errors through it.
601pub fn phase_diagnostic<E: PhaseError>(
602    module: &Name,
603    phase: &str,
604    errors: &[E],
605    file: Option<SourceFileId>,
606) -> Diagnostic<SourceFileId> {
607    let labels =
608        |errors: &[E]| spans_to_labels(errors.iter().flat_map(|e| e.labels()).collect(), file);
609
610    match errors {
611        [only] => Diagnostic::error()
612            .with_message(format!("[{}] {}", module, only.message()))
613            .with_labels(labels(errors))
614            .with_notes(only.notes()),
615        many => Diagnostic::error()
616            .with_message(format!(
617                "[{}] {} {} error{}",
618                module,
619                many.len(),
620                phase,
621                if many.len() == 1 { "" } else { "s" }
622            ))
623            .with_labels(labels(many))
624            .with_notes(many.iter().flat_map(|e| e.message_and_notes()).collect()),
625    }
626}
627
628/// Render an error that is about no place in any source — a path on disk, a program that
629/// could not be started — as its message and its notes, with no label.
630///
631/// `zelkova::BuildError::as_diagnostic` is its caller, for the errors of writing
632/// and running a build.
633pub fn plain_diagnostic<E: PhaseError>(error: &E) -> Diagnostic<SourceFileId> {
634    Diagnostic::error()
635        .with_message(error.message())
636        .with_notes(error.notes())
637}
638
639/// Every way checking a package can fail, tagged with the phase that failed.
640///
641/// What can go wrong once a package has checked — emitting it, writing it, running it —
642/// is `zelkova::BuildError`'s, which wraps this.
643///
644/// Each phase-carrying variant holds *all* the errors that phase produced for one
645/// module rather than only the first, plus the module's [`Name`], which is what
646/// `as_diagnostic` puts in front of the message. Rendering goes through
647/// [`PhaseError`]; see that trait for why no variant here carries a span.
648#[derive(Debug)]
649pub enum CompilationError {
650    /// The package's `zelkova.toml` is missing, malformed, or fails one of its own
651    /// field-level rules (an illegal `name`, a dependency entry naming no source, …).
652    ///
653    /// Raised before [`LoadingFiles`](CompilationError::LoadingFiles) even runs — a
654    /// package's source root is derived from its manifest, so there is nothing to walk
655    /// until the manifest is known good — with one exception:
656    /// [`PrivateModuleNotFound`](manifest::ManifestError::PrivateModuleNotFound) needs the
657    /// package's parsed modules and is pushed after them, and
658    /// [`MainModuleNotFound`](manifest::ManifestError::MainModuleNotFound) needs its checked
659    /// ones and is pushed after those.
660    ///
661    /// It carries no [`SourceFileId`] either way. A manifest error's location is a byte
662    /// range in `zelkova.toml`, and that file is never read into the database, so each
663    /// error names its manifest in its own message instead.
664    Manifest(Vec<manifest::ManifestError>),
665    /// The build could not be resolved: a dependency that could not be obtained or
666    /// read, a circular package graph, or two modules answering to one name in one
667    /// package.
668    ///
669    /// Like [`Manifest`](CompilationError::Manifest) it carries no
670    /// [`SourceFileId`]: what each of these errors is about is a `zelkova.toml`, which
671    /// is not a file the database holds, so each names its manifest in its own message.
672    /// The graph half is raised before any source is loaded and goes back to the caller
673    /// unrendered; the name-collision half is pushed onto [`check_package`]'s
674    /// accumulator once the packages' modules are known, and stops that package being
675    /// compiled.
676    Resolution(Vec<resolve::Error>),
677    LoadingFiles(Vec<SourceFileError>),
678    Source(parser::Error, SourceFileId),
679    Canonical(Vec<canonical::Error>, Name),
680    /// Type checking failed for the named module.
681    Type(Vec<typer::Error>, Name),
682    /// Exhaustiveness checking failed for the named module. Unreachable while
683    /// `exhaustiveness::check` is a stub, but rendered like any other phase.
684    Exhaustiveness(Vec<exhaustiveness::Error>, Name),
685    /// The named module is its package's `main`, checked, and is not fit to be a
686    /// program's entry point ([`program`]).
687    Program(Vec<program::Error>, Name),
688    DependenciesError(dependencies::Error),
689
690    /// An error together with the file the module it belongs to was read from.
691    ///
692    /// A phase never knows its [`SourceFileId`], so the labels its errors produce are
693    /// byte ranges with no file attached. This is where the two halves are put
694    /// together, and it has exactly one constructor: [`check_package`], which is
695    /// the only place that knows which file a module was parsed from. Nothing else
696    /// should build it — an id guessed anywhere else would underline the wrong file.
697    InFile(Box<CompilationError>, SourceFileId),
698
699    /// Several errors, rendered as one summary.
700    ///
701    /// Nothing in the compiler builds it: [`check_package`] hands its accumulated errors
702    /// back as [`PackageCheck::errors`], and the driver's own accumulation is
703    /// `zelkova::BuildError::Many`, which holds each of them on its own.
704    Many(Vec<CompilationError>),
705}
706
707impl CompilationError {
708    /// Turn this error into the diagnostic the user reads.
709    ///
710    /// This is the compiler's single rendering point — nothing else builds a `Diagnostic`
711    /// from a phase error, and `zelkova::BuildError::as_diagnostic` calls it for
712    /// every error of the check. It is public so
713    /// that a test can assert on what the user is actually shown, rather than on
714    /// `is_err()`: what a failure *says* is the behaviour this method exists for.
715    pub fn as_diagnostic(&self) -> Diagnostic<SourceFileId> {
716        self.as_diagnostic_in(None)
717    }
718
719    /// The name of the module this error belongs to, when it has one.
720    ///
721    /// [`check_package`] uses it to look up the file the module was read from, which
722    /// is how an [`InFile`](CompilationError::InFile) wrapper gets its id.
723    pub fn module(&self) -> Option<&Name> {
724        match self {
725            CompilationError::Canonical(_, module)
726            | CompilationError::Type(_, module)
727            | CompilationError::Exhaustiveness(_, module)
728            | CompilationError::Program(_, module) => Some(module),
729            CompilationError::InFile(inner, _) => inner.module(),
730            _ => None,
731        }
732    }
733
734    /// `as_diagnostic`, carrying the file the error's module was read from.
735    ///
736    /// `file` is `None` until an [`InFile`](CompilationError::InFile) wrapper supplies
737    /// one, which only [`check_package`] builds. Everything downstream of that
738    /// distinction is in `phase_diagnostic`, where it serves as the fallback file for
739    /// labels that do not name one themselves.
740    fn as_diagnostic_in(&self, file: Option<SourceFileId>) -> Diagnostic<SourceFileId> {
741        match self {
742            // The one arm that changes `file`, and the only one that can: it is the
743            // only variant that carries an id.
744            CompilationError::InFile(inner, id) => inner.as_diagnostic_in(Some(*id)),
745            // The one phase that carries spans renders its own labelled diagnostic.
746            CompilationError::Source(err, file_id) => err.diagnostic(*file_id),
747            // Only `PrivateModuleNotFound` and `MainModuleNotFound` ever reach this arm:
748            // every other `ManifestError` is raised before the file database exists and
749            // goes back to the caller unrendered. Those two are pushed after parsing, so a
750            // database does exist here — but the location they want is a byte range in
751            // `zelkova.toml`, which is not a file that database holds, so they render with
752            // no label and each error names itself and its manifest in a note.
753            CompilationError::Manifest(errors) => Diagnostic::error()
754                .with_message("Error in the package manifest")
755                .with_notes(errors.iter().flat_map(|e| e.message_and_notes()).collect()),
756            // A resolution error is about a manifest, the same as the arm above, and
757            // renders the same way: no label, because `zelkova.toml` is not in the file
758            // database. A lone error keeps its own headline — the collision message
759            // names both the module and the package, and burying it under a summary
760            // line would cost the user the one sentence that says what to change.
761            CompilationError::Resolution(errors) => match errors.as_slice() {
762                [only] => Diagnostic::error()
763                    .with_message(only.message())
764                    .with_notes(only.notes()),
765                many => Diagnostic::error()
766                    .with_message(format!(
767                        "{} error{} while resolving the build",
768                        many.len(),
769                        if many.len() == 1 { "" } else { "s" }
770                    ))
771                    .with_notes(many.iter().flat_map(|e| e.message_and_notes()).collect()),
772            },
773            // Loading failures are not attached to a module — there is no module yet,
774            // and each error names its own file instead.
775            CompilationError::LoadingFiles(errors) => Diagnostic::error()
776                .with_message("Error while loading the package files")
777                .with_notes(errors.iter().flat_map(|e| e.message_and_notes()).collect()),
778            CompilationError::Canonical(errors, module) => {
779                phase_diagnostic(module, "canonical", errors, file)
780            }
781            CompilationError::Type(errors, module) => {
782                phase_diagnostic(module, "type", errors, file)
783            }
784            CompilationError::Exhaustiveness(errors, module) => {
785                phase_diagnostic(module, "exhaustiveness", errors, file)
786            }
787            CompilationError::Program(errors, module) => {
788                phase_diagnostic(module, "program", errors, file)
789            }
790            // A dependency cycle belongs to the package, not to any one module, so it
791            // does not go through `phase_diagnostic` — there is no module to supply
792            // the fallback file that helper takes. Its labels don't need one: each
793            // `CycleEdge` (`ERR-6`) carries the file the `import` forming it was
794            // written in, so `spans_to_labels` is called with `None` and a label
795            // with no file of its own (an edge `ModuleWalker::new` couldn't map back
796            // to a file) is dropped rather than guessed at.
797            CompilationError::DependenciesError(err) => Diagnostic::error()
798                .with_message(err.message())
799                .with_labels(spans_to_labels(err.labels(), None))
800                .with_notes(err.notes()),
801            // Nothing in the compiler builds a `Many` (see the variant), so this arm only
802            // fires when one built elsewhere is rendered as a whole. It summarises rather
803            // than flattening its members' labels, for the reason given at
804            // `zelkova::BuildError::Many`, the group a build actually renders.
805            CompilationError::Many(errors) => Diagnostic::error()
806                .with_message(format!(
807                    "compilation failed with {} error{}",
808                    errors.len(),
809                    if errors.len() == 1 { "" } else { "s" }
810                ))
811                .with_notes(
812                    errors
813                        .iter()
814                        .map(|e| e.as_diagnostic_in(file).message)
815                        .collect(),
816                ),
817        }
818    }
819
820    fn from(err: parser::Error, source_id: SourceFileId) -> Self {
821        CompilationError::Source(err, source_id)
822    }
823}
824
825// There is deliberately no `From<typer::Error>` or `From<exhaustiveness::Error>`
826// here. Both used to exist and neither could be written honestly: a `CompilationError`
827// needs the name of the module its error belongs to, and a phase error does not know
828// it. `From` has nowhere to get it from, so the two impls lost information instead —
829// one discarded the error and the other panicked. `check_module_recovering` is the one place that
830// knows both halves, so that is where the conversion happens (see `ERR-2` in
831// `docs/tickets/README.md`).
832
833impl From<dependencies::Error> for CompilationError {
834    fn from(err: dependencies::Error) -> Self {
835        CompilationError::DependenciesError(err)
836    }
837}
838
839impl From<Vec<SourceFileError>> for CompilationError {
840    fn from(errors: Vec<SourceFileError>) -> Self {
841        CompilationError::LoadingFiles(errors)
842    }
843}
844
845impl From<Vec<manifest::ManifestError>> for CompilationError {
846    fn from(errors: Vec<manifest::ManifestError>) -> Self {
847        CompilationError::Manifest(errors)
848    }
849}
850
851impl From<Vec<resolve::Error>> for CompilationError {
852    fn from(errors: Vec<resolve::Error>) -> Self {
853        CompilationError::Resolution(errors)
854    }
855}
856
857/// Which of the root package's source roots a build compiles.
858///
859/// A package has two, `src/` and `tests/`, and only the first is ever compiled for a
860/// package that is being depended on: a dependency's `tests/` is not read, not resolved
861/// and not observable ([*Tests*](../docs/spec/packages.md#tests)). So this is a
862/// property of the build rather than of a package, and it applies to the package the
863/// compiler was pointed at.
864#[derive(Debug, Clone, Copy, PartialEq, Eq)]
865enum TestRoot {
866    /// `src/` alone.
867    Skipped,
868    /// Both roots. The modules under `tests/` are checked against the package's own
869    /// modules — the private ones included — and the public modules of both dependency
870    /// maps.
871    Compiled,
872}
873
874/// One module of a build the check built a tree for, with the file it was read from and
875/// the source root it was read under. Which list of [`PackageCheck`] holds it says
876/// whether it checked.
877pub struct CheckedSource {
878    /// The module, as [`check_module_recovering`] handed it back.
879    pub module: CheckedModule,
880    /// The file it was read from, which an error's labels about it point into. `None` for
881    /// no module today, since only a module that parsed from a file can be checked.
882    pub file: Option<SourceFileId>,
883    /// The directory of the source root it was read under — a package's `src/` or
884    /// `tests/`. A facade's companion sits below it at the same path the facade does
885    /// ([*A facade names a boundary, not a
886    /// backend*](../docs/spec/interop.md#a-facade-names-a-boundary-not-a-backend)), and
887    /// nothing here looks for one: that is the build's business, not the check's.
888    pub root_dir: PathBuf,
889}
890
891/// One status line of a check, in the order the check reached it: `parsed 8 modules`,
892/// `checked modules: [..]`, or what failed in either.
893///
894/// [`check_package`] records these rather than printing them.
895/// `zelkova::compile_package` prints each one to stderr, after `success` or
896/// `failure` in bold green or red, before anything else it prints.
897#[derive(Debug, Clone, PartialEq, Eq)]
898pub struct Status {
899    /// Whether the phase it reports got through without an error.
900    pub success: bool,
901    /// The line after `success` or `failure`.
902    pub text: String,
903}
904
905/// What [`check_package`] found: everything a build needs to emit, and everything that
906/// went wrong, with nothing printed and nothing written.
907pub struct PackageCheck {
908    /// The package the check was pointed at.
909    pub package: PackageName,
910    /// Every error the check accumulated, across every package of the build. Empty means
911    /// the build checked. Each renders through [`CompilationError::as_diagnostic`], whose
912    /// labels index into [`sources`](PackageCheck::sources).
913    pub errors: Vec<CompilationError>,
914    /// The one file database of the build: every file of every package that was read,
915    /// which is what every [`SourceFileId`] in [`errors`](PackageCheck::errors) and in the
916    /// modules below indexes into.
917    pub sources: SourceFiles,
918    /// Every module that checked of every package the plain build holds — the root's
919    /// `src/` and its `dependencies`, direct or not — dependencies first. A package whose
920    /// own `src/` was checked and reported an error in it contributes none of its
921    /// modules here: the ones it built a tree for are in
922    /// [`failing`](PackageCheck::failing). A package that was never checked, because a
923    /// dependency did not compile, its sources could not be read or two of its modules
924    /// claim one name, has its modules in no list at all. An error from the package's
925    /// `main` alone ([`program`] has the check) leaves its modules here.
926    pub modules: Vec<CheckedSource>,
927    /// Every module that checked of every package the build reaches through the root's
928    /// `test-dependencies` alone. Empty unless the tests were checked
929    /// ([`check_package_with_tests`]).
930    pub test_dependency_modules: Vec<CheckedSource>,
931    /// Every module of the root's `tests/` that checked. Empty unless the tests were
932    /// checked, and unless the root's `src/` checked.
933    pub test_modules: Vec<CheckedSource>,
934    /// Every module the check built a tree for and put in none of
935    /// [`modules`](PackageCheck::modules),
936    /// [`test_dependency_modules`](PackageCheck::test_dependency_modules) and
937    /// [`test_modules`](PackageCheck::test_modules): each module of a package whose own
938    /// `src/` was checked and reported an error, whether or not the module has an error
939    /// of its own, and each module of the root's `tests/` that came back with errors. A
940    /// module of a package that was not checked at all is in none of them, for the
941    /// reasons [`modules`](PackageCheck::modules) lists.
942    ///
943    /// They are for a reader that wants the typed tree of a file with errors in it, such
944    /// as an editor, and never for a backend: an emitting build reads the three lists
945    /// above, and an IR built here lists every declaration canonicalization recorded as
946    /// broken or the typer rejected in [`ir::Module::unchecked`], which no backend emits
947    /// ([`DEC-23` decision 5](../docs/decisions/dec-23.md)).
948    pub failing: Vec<CheckedSource>,
949    /// The status lines of the check, in order.
950    pub status: Vec<Status>,
951}
952
953/// Check the package rooted at `package_dir` and every package it depends on, as
954/// `zelkova::compile_package` does, and hand back what was found without printing
955/// or writing anything.
956///
957/// This is the checking half of every `compile_package` variant: they call it, then print
958/// its [`status`](PackageCheck::status) lines, emit and write a build that checked, and
959/// render every error. It reads every manifest from disk and every `.zel` source of the build
960/// from `overlay` where it holds the file and from disk otherwise ([`Overlay`] has the
961/// rules, and the CLI half passes an empty one), and nothing else: it does not look for a
962/// facade's companion, which only a build that emits needs. It never writes to stderr, to stdout or to disk, and never builds a
963/// `Diagnostic` — a caller that wants one asks each error for it.
964///
965/// `Err` is an error raised before there is a build to check — the root's manifest, or
966/// the resolution of its dependencies — and comes with no file database, since no source
967/// was read. Every later error is in [`PackageCheck::errors`], and an `Ok` whose `errors`
968/// is not empty is a package that did not check.
969///
970/// The root's `tests/` is not checked; [`check_package_with_tests`] is that check.
971pub fn check_package(
972    package_dir: &Path,
973    overlay: &Overlay,
974) -> Result<PackageCheck, CompilationError> {
975    check(package_dir, TestRoot::Skipped, overlay)
976}
977
978/// [`check_package`], checking the root package's `tests/` as well as its `src/`, as
979/// `zelkova::compile_package_with_tests` does.
980pub fn check_package_with_tests(
981    package_dir: &Path,
982    overlay: &Overlay,
983) -> Result<PackageCheck, CompilationError> {
984    check(package_dir, TestRoot::Compiled, overlay)
985}
986
987/// The checking half of a build, behind [`check_package`] and [`check_package_with_tests`]:
988/// every step of this module's documentation, for every package of the build, recording
989/// what it finds.
990fn check(
991    package_dir: &Path,
992    tests: TestRoot,
993    overlay: &Overlay,
994) -> Result<PackageCheck, CompilationError> {
995    // The status line of every phase, in order, for the caller to print or not.
996    let mut status: Vec<Status> = Vec::new();
997
998    // Step 1: read and validate the root package's manifest. This is the one failure
999    // that cannot be deferred to the usual accumulate-and-render path, because it
1000    // happens before that path exists: the source root itself is derived from the
1001    // manifest, so there is no build to walk, no accumulator and no file database yet.
1002    // It goes back to the caller as `Err`, and the driver returns it unrendered. Every
1003    // failure after the accumulator below is created goes onto it instead, this package's
1004    // source loading included.
1005    debug!("phase: read package manifest");
1006    let manifest = manifest::load(package_dir)?;
1007    // The package the compiler was pointed at, which is the one whose `tests/` root
1008    // this build compiles — every other package in the build is here because something
1009    // depends on it, and a dependency's tests are never compiled.
1010    let root_package = manifest.name.clone();
1011
1012    // Step 1b: resolve the build. Every package reachable from this one's two dependency
1013    // maps, each ordered after the packages it depends on, so an
1014    // `Interface` a package needs always exists by the time that package is
1015    // compiled. The union is resolved whether or not the tests are being compiled, so
1016    // that one version of each package and an acyclic graph are settled once for the
1017    // build. A dependency's own manifest is read here, so this is raised before any
1018    // source is loaded and goes back unrendered for the same reason the manifest above
1019    // does.
1020    debug!("phase: resolve the build");
1021    let build = resolve::resolve(package_dir, manifest)?;
1022
1023    // Every package this build reaches only through `test-dependencies` — never through
1024    // the plain `dependencies` graph. None of them is compiled by the first loop below.
1025    //
1026    // A build that did not ask for the tests has no use for a `test-dependency` at all:
1027    // no module here can import one, since a `test-dependency`'s modules are held out of
1028    // the environment `src/` is checked against. Compiling it anyway would parse and
1029    // check a package the user never reached for, record its status lines, and fail this
1030    // build on an error inside it. One that did ask for the tests compiles it in the
1031    // second loop — the tests need its interface — but does not write it.
1032    let test_only = resolve::test_only_packages(&build, &root_package);
1033
1034    // Further steps will produce errors. We aggregate them here and hand them back,
1035    // rather than stopping on the first one, so that a single broken module doesn't hide
1036    // the diagnostics of every other module.
1037    //
1038    // They are kept as typed `CompilationError`s and not as already-rendered
1039    // `Diagnostic`s for two reasons: `as_diagnostic` stays the single rendering point,
1040    // and the accumulation is still meaningful as a return value — an empty vector is
1041    // what makes the driver return `Ok`.
1042    let mut errors: Vec<CompilationError> = vec![];
1043
1044    // One file database for the whole build, so that a diagnostic about any module of
1045    // any package renders against the file it was written in. `SourceFileId` is an
1046    // index into this one database, and it travels inside `Interface`s that outlive
1047    // the package they came from (`ERR-5`), which is exactly why there is one
1048    // database and not one per package.
1049    let mut sources = SourceFiles::new();
1050
1051    // What each compiled package offers its dependents: its public modules'
1052    // `Interface`s, keyed by the module's name *within* that package. The spelling a
1053    // depending package reaches one by — `AcmeWidgets.Size`, or `Size` when it
1054    // unwraps — belongs to that package alone and is applied by `compile_in_build`.
1055    let mut published: HashMap<PackageName, HashMap<Name, Interface>> = HashMap::new();
1056
1057    // Every module that checked, from every package that belongs in the build's output,
1058    // which the driver holds back from emission until the whole build is known to have
1059    // checked: a module cannot be written while another may still fail. A
1060    // `test-dependency`-only package is compiled when
1061    // the tests ask for it, but its modules never land here — only the first loop below
1062    // extends it.
1063    let mut checked: Vec<CheckedSource> = Vec::new();
1064
1065    // Every module of each test-only package that checked. Stays empty for a build that
1066    // did not ask for the tests.
1067    let mut test_dependency_modules: Vec<CheckedSource> = Vec::new();
1068
1069    // Every module of the root's `tests/` that checked. Stays empty for a build that did
1070    // not ask for the tests, or whose root `src/` did not check.
1071    let mut test_modules: Vec<CheckedSource> = Vec::new();
1072
1073    // Every module the check built a tree for that none of the three lists above holds.
1074    let mut failing: Vec<CheckedSource> = Vec::new();
1075
1076    // The root package's `tests/` is compiled apart from its `src/`, after every
1077    // test-only package, because a test-only package may depend on the root: that edge
1078    // names the root's `src/`, and the root's `tests/` in turn needs the test-only
1079    // package ([*`test-dependencies`*](../docs/spec/packages.md#test-dependencies)).
1080    // So the build runs in three steps: every package the plain graph reaches, the root's
1081    // `src/` last among them; then each test-only package, which sees the root through
1082    // `published` like any other dependency; then the root's `tests/`, against what its
1083    // `src/` left here.
1084    let mut root_tests: Option<(&resolve::ResolvedPackage, TestsEnvironment)> = None;
1085
1086    for package in &build {
1087        if test_only.contains(&package.name) {
1088            debug!(
1089                "phase: defer package {} — reached through `test-dependencies` alone",
1090                package.name
1091            );
1092            continue;
1093        }
1094
1095        debug!("phase: compile package {}", package.name);
1096
1097        let tests = if package.name == root_package {
1098            tests
1099        } else {
1100            TestRoot::Skipped
1101        };
1102
1103        if let Some(compiled) = compile_in_build(
1104            package,
1105            &build,
1106            &published,
1107            tests,
1108            overlay,
1109            &mut sources,
1110            &mut errors,
1111            &mut failing,
1112            &mut status,
1113        ) {
1114            published.insert(package.name.clone(), compiled.public);
1115            checked.extend(compiled.modules);
1116            if let Some(environment) = compiled.tests {
1117                root_tests = Some((package, environment));
1118            }
1119        }
1120    }
1121
1122    // `None` unless the tests were asked for and the root's `src/` checked, and in the
1123    // second case the failure is already reported: its tests would only be blamed for a
1124    // type the package never managed to declare. A test-only package is in the build for
1125    // the tests alone, so it is not compiled either — one that depends on the root would
1126    // only add a `DependencyNotCompiled` saying what the root's own errors already say.
1127    if let Some((root, environment)) = root_tests {
1128        for package in build.iter().filter(|p| test_only.contains(&p.name)) {
1129            debug!("phase: compile package {} for the tests", package.name);
1130
1131            // Compiled and published so the tests can be checked against it. Nothing
1132            // outside a package's own tests reads its modules, so they never join
1133            // `checked` — the plain build's output tree — but they do join
1134            // `test_dependency_modules`, which only the test tree reads.
1135            if let Some(compiled) = compile_in_build(
1136                package,
1137                &build,
1138                &published,
1139                TestRoot::Skipped,
1140                overlay,
1141                &mut sources,
1142                &mut errors,
1143                &mut failing,
1144                &mut status,
1145            ) {
1146                published.insert(package.name.clone(), compiled.public);
1147                test_dependency_modules.extend(compiled.modules);
1148            }
1149        }
1150
1151        debug!("phase: compile the tests of package {}", root.name);
1152        test_modules = compile_tests(
1153            root,
1154            &build,
1155            &published,
1156            environment,
1157            &mut errors,
1158            &mut failing,
1159            &mut status,
1160        );
1161    }
1162
1163    Ok(PackageCheck {
1164        package: root_package,
1165        errors,
1166        sources,
1167        modules: checked,
1168        test_dependency_modules,
1169        test_modules,
1170        failing,
1171        status,
1172    })
1173}
1174
1175/// Pair each checked module of one source root with the file it was read from and
1176/// `root_dir`, the source root's own directory, `src/` or `tests/`.
1177///
1178/// Shared by [`compile_in_build`] (over `src/`) and [`compile_tests`] (over `tests/`),
1179/// which differ only in which root's directory and file map they pass.
1180fn to_checked_sources(
1181    checked: Vec<CheckedModule>,
1182    root_dir: &Path,
1183    module_files: &HashMap<Name, SourceFileId>,
1184) -> Vec<CheckedSource> {
1185    checked
1186        .into_iter()
1187        .map(|module| CheckedSource {
1188            file: module_files.get(module.canonical.name.name()).copied(),
1189            root_dir: root_dir.to_path_buf(),
1190            module,
1191        })
1192        .collect()
1193}
1194
1195/// What [`compile_in_build`] hands back for a package whose every module checked.
1196struct CompiledPackage {
1197    /// What it offers its dependents: its public modules' `Interface`s, keyed by each
1198    /// module's name within the package.
1199    public: HashMap<Name, Interface>,
1200    /// Every module of it that was compiled, to be emitted once the whole build is known
1201    /// to have checked.
1202    modules: Vec<CheckedSource>,
1203    /// What [`compile_tests`] needs to check this package's `tests/` later. `Some` only
1204    /// when [`compile_in_build`] was handed [`TestRoot::Compiled`].
1205    tests: Option<TestsEnvironment>,
1206}
1207
1208/// What [`compile_in_build`] leaves behind for a package's `tests/`, which is parsed
1209/// alongside `src/` and checked in a later step of the build ([`compile_tests`]) once
1210/// every test-only package is compiled.
1211struct TestsEnvironment {
1212    /// Every interface `src/` was checked against or produced: the package's own modules,
1213    /// the private ones and the facades included, and its `dependencies`' public modules
1214    /// under the spelling this package names them by. A test module sees all of it.
1215    interfaces: HashMap<Name, Interface>,
1216    /// The modules of `tests/`, as the parser left them.
1217    modules: Vec<parser::Module>,
1218    /// The modules both roots declare, `src/` first, each with its file. They were
1219    /// already checked against each other and against the plain dependencies; the tests'
1220    /// own collision check runs them again with the `test-dependencies` added.
1221    local_modules: Vec<resolve::LocalModule>,
1222    /// The file each module of either root was parsed from.
1223    module_files: HashMap<Name, SourceFileId>,
1224}
1225
1226/// Compile the `src/` root of one package of a resolved build, and hand back what it
1227/// offers its dependents and the modules it compiled.
1228///
1229/// `published` holds what every package compiled before this one offers — this
1230/// package's dependencies among them, since `check` calls this for a package only
1231/// once everything it depends on has had its turn. `sources` and `errors` are the
1232/// build's, not this package's: every module of every package renders against one file
1233/// database, and every error from any package makes the build fail.
1234///
1235/// `tests` says whether this package's `tests/` root is compiled later in the build. It
1236/// is [`TestRoot::Compiled`] for the package the compiler was pointed at, when the
1237/// caller asked for tests, and for no other package in the build. With
1238/// [`TestRoot::Compiled`] this function reads and parses `tests/`, so that a module name
1239/// the two roots share is reported before `src/` is checked, but checks none of it: it
1240/// hands back the parsed modules in the [`TestsEnvironment`] that [`compile_tests`]
1241/// checks them against. A test module that fails to parse fails this package, the same
1242/// as one of `src/` would. The two roots are
1243/// two environments: a module under `src/` sees neither a test module nor a
1244/// `test-dependency`'s — an import naming one is a module that does not exist, the same
1245/// as a private module of another package.
1246///
1247/// `None` means this package did not check and publishes nothing. It always comes with
1248/// at least one error already pushed onto `errors` — a dependency that did not compile,
1249/// sources that could not be read, a name claimed twice, or a failure in one of this
1250/// package's own modules — so a package that publishes nothing can never be read as one
1251/// that compiled. A package whose modules failed publishes nothing to its dependents:
1252/// they would otherwise be checked against half a package and report errors belonging
1253/// to a module they never wrote. Within the package it is otherwise: a module with an
1254/// error in it was still checked against the interfaces of the modules before it, and
1255/// every module of `src/` the check built a tree for, checked or not, is pushed onto
1256/// `failing` ([`PackageCheck::failing`]).
1257///
1258/// There is deliberately no error return. Every way this can fail is a diagnostic about
1259/// one package of a build whose other packages may already have accumulated diagnostics
1260/// of their own, and a `Result` here is an invitation to `?` those out of
1261/// `zelkova::compile_package` past its reporting loop — which is the "nothing is
1262/// rendered and then dropped" the accumulator exists to prevent.
1263// Nine parameters, two over clippy's limit. They are the build, what it published, and the
1264// four accumulators a package's check appends to; none of them groups with another without
1265// a type that exists only to pass them along.
1266#[allow(clippy::too_many_arguments)]
1267fn compile_in_build(
1268    package: &resolve::ResolvedPackage,
1269    build: &[resolve::ResolvedPackage],
1270    published: &HashMap<PackageName, HashMap<Name, Interface>>,
1271    tests: TestRoot,
1272    overlay: &Overlay,
1273    sources: &mut SourceFiles,
1274    errors: &mut Vec<CompilationError>,
1275    failing: &mut Vec<CheckedSource>,
1276    status: &mut Vec<Status>,
1277) -> Option<CompiledPackage> {
1278    let errors_before = errors.len();
1279
1280    // Step 1: what this package is compiled against. Only its *direct* dependencies:
1281    // a package listed in a dependency's manifest and not in this one's is in the
1282    // build and is not importable here
1283    // (`docs/spec/packages.md#only-direct-dependencies-are-usable`).
1284    //
1285    // `test-dependencies` are not among them. They are resolved into the build whether or
1286    // not the tests are compiled (`docs/spec/packages.md#test-dependencies`), and join
1287    // the environment in `compile_tests`, which checks `tests/` alone.
1288    let dependencies = direct_dependencies(
1289        package,
1290        package.manifest.dependencies.iter(),
1291        build,
1292        published,
1293        errors,
1294    )?;
1295
1296    // Step 2 and 3.a
1297    debug!("phase: load package sources");
1298    // A package whose sources cannot be read is not compiled, and nothing of it is
1299    // published — but the failure is accumulated like any other rather than returned,
1300    // because by the time we get here other packages of the build may already have
1301    // pushed diagnostics onto `errors` and returning would carry this one past the
1302    // reporting loop and drop theirs ("nothing is rendered and then dropped").
1303    //
1304    // A loading error names a path and has no span, so it renders against the shared
1305    // database whether or not this package contributed a single file to it.
1306    let src_ids = match source::load_package_sources_into(
1307        &package.root,
1308        source::SourceRoot::Src,
1309        Some(&package.name),
1310        overlay,
1311        sources,
1312    ) {
1313        Ok(file_ids) => file_ids,
1314        Err(error) => {
1315            errors.push(error);
1316            return None;
1317        }
1318    };
1319
1320    // Step 3.b
1321    debug!("phase: parse package sources");
1322    let parsed = parse_root(&src_ids, sources, errors);
1323    record_parse_status(&parsed, "modules", status);
1324
1325    // `tests/` is read and parsed here, ahead of anything in `src/` being checked, even
1326    // though it is checked last in the build (`compile_tests`): the two roots share one
1327    // set of module names, and a collision between them has to be reported before any
1328    // module of the package is compiled (step 3.d below). A package that holds no
1329    // `tests/` directory at all is a package with no tests rather than a failure —
1330    // unlike `src/`, which every package has.
1331    let parsed_tests = match tests {
1332        TestRoot::Skipped => None,
1333        TestRoot::Compiled => {
1334            debug!("phase: load package test sources");
1335            let test_ids = match source::load_package_sources_into(
1336                &package.root,
1337                source::SourceRoot::Tests,
1338                Some(&package.name),
1339                overlay,
1340                sources,
1341            ) {
1342                Ok(file_ids) => file_ids,
1343                Err(error) => {
1344                    errors.push(error);
1345                    return None;
1346                }
1347            };
1348
1349            debug!("phase: parse package test sources");
1350            let parsed_tests = parse_root(&test_ids, sources, errors);
1351            record_parse_status(&parsed_tests, "test modules", status);
1352            Some(parsed_tests)
1353        }
1354    };
1355
1356    // Step 3.c
1357    // TODO Verify modules name match file system.
1358    // TODO Include this into the parser::parse() function (w/ module name as argument) ?
1359
1360    // `private-modules` can only be checked against the package's real modules once they
1361    // are parsed, which is why this lives here rather than inside `manifest::load` — every
1362    // other manifest error is known from the manifest text alone. What the list is *for*
1363    // is a few lines down: a module it names is kept out of what this package publishes.
1364    //
1365    // It is checked against `src/` alone. The list says what the package does not ship,
1366    // and a test module is not shipped by anything, so naming one there names a module
1367    // this package does not hold — the same answer whether or not the tests are being
1368    // compiled.
1369    //
1370    // A file whose header failed to parse contributes no module, so with one the list of
1371    // modules the package holds is known to be short. Checking against it then blames the
1372    // manifest for the parser's failure, and the parse error is already on its way to the
1373    // user, so the check is skipped entirely rather than run on a list it cannot trust.
1374    if parsed.headless.is_empty() {
1375        let held_modules: std::collections::HashSet<&Name> =
1376            parsed.modules.iter().map(|m| &m.name).collect();
1377        let missing_private_modules: Vec<manifest::ManifestError> = package
1378            .manifest
1379            .private_modules
1380            .iter()
1381            .filter(|name| !held_modules.contains(name))
1382            .cloned()
1383            .map(|name| manifest::ManifestError::PrivateModuleNotFound {
1384                manifest_path: package.manifest_path(),
1385                name,
1386            })
1387            .collect();
1388        if !missing_private_modules.is_empty() {
1389            errors.push(CompilationError::Manifest(missing_private_modules));
1390        }
1391    }
1392
1393    // Step 3.d: the whole map of module names this package can import, built before
1394    // anything is canonicalized. A name two modules both answer to is the manifest's
1395    // doing, not any one file's, so it is reported here and no module of the package is
1396    // canonicalized, type checked or checked for exhaustiveness
1397    // (`docs/spec/packages.md#two-modules-under-one-name-is-an-error`).
1398    //
1399    // The files have been read and parsed by this point, because `local_modules` is
1400    // taken from the parsed module headers. `SourceFile` derives a module name from the
1401    // relative path alone, so this could run one step earlier, against the walk; the
1402    // reason it does not is that the parsed header is what every other phase calls a
1403    // module by.
1404    //
1405    // Whichever roots this build read are in the list, `src/` first — the order a
1406    // collision between the two reads best in — because the two share one set of names:
1407    // `src/Model.zel` and `tests/Model.zel` are both `Model`, and the chapter calls that
1408    // the same error as two modules of one root answering to one name
1409    // (`docs/spec/packages.md#source-roots`). A `tests/` root is read [when this
1410    // package's own tests are run and at no other
1411    // time](../docs/spec/packages.md#tests), so an ordinary build has not read the
1412    // second file and has nothing to report.
1413    //
1414    // The dependencies here are the plain ones alone. A collision with a
1415    // `test-dependency`'s module is found by `compile_tests`, because a test-dependency
1416    // may depend on this package and so is compiled after it: what it publishes is not
1417    // known yet. That half is reported late, which the chapter records as a known gap
1418    // (`docs/tickets/bug-42.md`).
1419    let mut local_modules = parsed.local_modules.clone();
1420    if let Some(parsed_tests) = &parsed_tests {
1421        local_modules.extend(parsed_tests.local_modules.iter().cloned());
1422    }
1423    let visible = match resolve::visible_modules(package, &local_modules, &dependencies) {
1424        Ok(visible) => visible,
1425        // As with the uncompiled-dependency case in `direct_dependencies`, the diagnostic
1426        // is the whole report: a second, shorter copy on the status line said nothing it
1427        // does not.
1428        Err(collisions) => {
1429            errors.push(CompilationError::Resolution(collisions));
1430            return None;
1431        }
1432    };
1433
1434    // Each dependency's public modules enter the environment under the spelling this
1435    // package names them by, and under no other: a wrapped dependency's `Size` is
1436    // `AcmeWidgets.Size` here and nothing else, an unwrapped one's is `Size` and
1437    // nothing else. The `Interface` is the same either way — it carries the module's
1438    // own name, never the spelling — which is what keeps two packages that spell one
1439    // dependency differently agreeing about every type in it.
1440    let mut interfaces: HashMap<Name, Interface> = HashMap::new();
1441    insert_dependency_interfaces(package, &visible, published, |_| true, &mut interfaces);
1442
1443    // A module whose header did not parse still exists, under the name its path gives
1444    // it, and an importer of it finds an interface that holds nothing and is incomplete.
1445    // The header's syntax error is the one error that stands behind everything the
1446    // importer then does not report, and it fails this package, so the stand-in is
1447    // never published. A name a parsed module declares is that module's, and so is a name
1448    // a dependency's interface already holds: a headless file named like one is a
1449    // collision whatever its header says, and the dependency's interface is the better
1450    // guess for every importer, which keeps the errors of those importers that the
1451    // header's syntax error does not explain.
1452    for (name, file) in &parsed.headless {
1453        if parsed.module_files.contains_key(name) || interfaces.contains_key(name) {
1454            continue;
1455        }
1456        let module_name = ModuleName {
1457            package: package.name.clone(),
1458            name: name.clone(),
1459        };
1460        interfaces.insert(name.clone(), Interface::unavailable(module_name, *file));
1461    }
1462
1463    // Whether this package is exempt from the default imports, and so receives none
1464    // of them, is `package.name.is_core()` — a property of the package's own name,
1465    // not of which modules either root happens to hold. So it cannot differ between
1466    // `src/` and `tests/`, or between a build that compiles the tests and one that
1467    // does not: both roots, and both walkers, are handed `&package.name` directly
1468    // rather than a flag computed once and threaded through.
1469
1470    // Steps 4 and 5.
1471    let RootCheck {
1472        checked: can_mods,
1473        failing: failing_mods,
1474    } = check_root(
1475        package,
1476        source::SourceRoot::Src,
1477        &parsed.modules,
1478        &parsed.module_files,
1479        &mut interfaces,
1480        errors,
1481        status,
1482    );
1483
1484    // A companion sits beside its facade's source, under the same path below the
1485    // source root that the facade's own emitted module has below its package's output
1486    // directory.
1487    //
1488    // Only `src/` feeds `checked` here — the plain build's output tree. A module of
1489    // `tests/` checks like any other but is not this package's, so it is not among
1490    // these: it joins the test tree's own modules later, in `compile_tests`.
1491    let root_dir = package.root.join(source::SourceRoot::Src.directory());
1492    let checked: Vec<CheckedSource> = to_checked_sources(can_mods, &root_dir, &parsed.module_files);
1493
1494    // A package with an error publishes nothing and emits nothing, so none of its modules
1495    // joins `checked` — but each one the check built a tree for is handed back, for a
1496    // reader that wants the tree of a file with an error in it.
1497    if errors.len() != errors_before {
1498        failing.extend(checked);
1499        failing.extend(to_checked_sources(
1500            failing_mods,
1501            &root_dir,
1502            &parsed.module_files,
1503        ));
1504        return None;
1505    }
1506
1507    // Step 5b: a program's entry point ([`program`]). It runs only once every module of
1508    // `src/` has checked, since the check reads the type inference solved for `main`, and
1509    // for every package of the build that declares `main` — the root and any dependency
1510    // alike; `program`'s documentation says why.
1511    //
1512    // A failure here fails the build, and the package is still published: its interfaces
1513    // are sound, and a dependent checked against them reports only its own errors.
1514    if let Some(main) = &package.manifest.main {
1515        check_main(package, main, &checked, errors);
1516    }
1517
1518    // Step 6: what this package offers whoever depends on it. Every module of `src/`
1519    // except the ones `private-modules` names and every `module foreign` facade, which
1520    // is package-internal by its own declaration whatever the manifest says
1521    // (`docs/spec/packages.md#what-a-package-exposes`, `docs/spec/interop.md`). A name
1522    // that would have reached one of those is simply absent from a dependent's map, so
1523    // it fails there as a module that does not exist.
1524    let facades: std::collections::HashSet<&Name> = parsed
1525        .modules
1526        .iter()
1527        .filter(|module| module.binding_foreign)
1528        .map(|module| &module.name)
1529        .collect();
1530    let private: std::collections::HashSet<&Name> =
1531        package.manifest.private_modules.iter().collect();
1532
1533    let public = interfaces
1534        .iter()
1535        // The map still holds the dependencies' interfaces seeded above; a package
1536        // publishes its own modules and nothing else, which is what stops it
1537        // re-exporting a dependency's module as one of its own
1538        // (`docs/spec/packages.md#what-a-package-boundary-cannot-rename`).
1539        .filter(|(name, interface)| {
1540            interface.module_name.package == package.name
1541                && !facades.contains(name)
1542                && !private.contains(name)
1543        })
1544        .map(|(name, interface)| (name.clone(), interface.clone()))
1545        .collect();
1546
1547    // `tests/` is checked against everything `src/` was checked against or produced —
1548    // the private modules included, since `interfaces` holds every module of the
1549    // package and the `private-modules` filter applies only to what is published.
1550    let tests = parsed_tests.map(|parsed_tests| {
1551        let mut module_files = parsed.module_files;
1552        module_files.extend(parsed_tests.module_files);
1553        TestsEnvironment {
1554            interfaces,
1555            modules: parsed_tests.modules,
1556            local_modules,
1557            module_files,
1558        }
1559    });
1560
1561    Some(CompiledPackage {
1562        public,
1563        modules: checked,
1564        tests,
1565    })
1566}
1567
1568/// Check the module the manifest's `main` names among `checked`, the modules of
1569/// `package`'s `src/` that checked, and push what is wrong with it onto `errors`.
1570///
1571/// A name matching no module of `src/` is the manifest's error, not a module's, so it is a
1572/// [`ManifestError::MainModuleNotFound`](manifest::ManifestError::MainModuleNotFound) and
1573/// renders with no location. `checked` holds `src/` alone, which is what keeps a module
1574/// under `tests/` from counting. Anything wrong with the module it finds is a
1575/// [`CompilationError::Program`] carrying that module's file, so its labels point into it.
1576fn check_main(
1577    package: &resolve::ResolvedPackage,
1578    main: &Name,
1579    checked: &[CheckedSource],
1580    errors: &mut Vec<CompilationError>,
1581) {
1582    let Some(entry) = checked
1583        .iter()
1584        .find(|to_emit| to_emit.module.canonical.name.name() == main)
1585    else {
1586        errors.push(CompilationError::Manifest(vec![
1587            manifest::ManifestError::MainModuleNotFound {
1588                manifest_path: package.manifest_path(),
1589                name: main.clone(),
1590            },
1591        ]));
1592        return;
1593    };
1594
1595    if let Err(error) = program::check(&entry.module) {
1596        let error = CompilationError::Program(vec![error], main.clone());
1597        errors.push(match entry.file {
1598            Some(file) => CompilationError::InFile(Box::new(error), file),
1599            None => error,
1600        });
1601    }
1602}
1603
1604/// Check the `tests/` root of the package the compiler was pointed at, which
1605/// [`compile_in_build`] already parsed into `environment`, against what its `src/` left
1606/// there and the public modules of both its dependency maps.
1607///
1608/// `check` calls this after the package's `src/` and after every test-only package,
1609/// because a test-only package may depend on this one
1610/// ([*`test-dependencies`*](../docs/spec/packages.md#test-dependencies)): `published`
1611/// then holds every package `tests/` can name. Only for a package whose `src/` checked —
1612/// a package whose own modules did not check cannot say anything true about its tests,
1613/// since every one of them would be blamed for a type the package never managed to
1614/// declare.
1615///
1616/// Every `tests/` module that checked, as a [`CheckedSource`] — the same shape
1617/// [`compile_in_build`] hands back for `src/` — so the test tree
1618/// (`zelkova::compile_package_with_tests`) can write them beside it. Empty, with
1619/// nothing published or emitted, for a package whose collision check or dependency
1620/// resolution failed before any module was checked. A test module is checked like any
1621/// other and a failure in one fails the build, but nothing outside the package's own
1622/// tests reads it, so it is never published to a dependent. A test module that came
1623/// back with errors is pushed onto `failing` ([`PackageCheck::failing`]).
1624fn compile_tests(
1625    package: &resolve::ResolvedPackage,
1626    build: &[resolve::ResolvedPackage],
1627    published: &HashMap<PackageName, HashMap<Name, Interface>>,
1628    environment: TestsEnvironment,
1629    errors: &mut Vec<CompilationError>,
1630    failing: &mut Vec<CheckedSource>,
1631    status: &mut Vec<Status>,
1632) -> Vec<CheckedSource> {
1633    let TestsEnvironment {
1634        mut interfaces,
1635        modules,
1636        local_modules,
1637        module_files,
1638    } = environment;
1639
1640    // Both dependency maps, the plain one included: the collision check below is over
1641    // everything `tests/` can name, and a plain dependency's module can collide with a
1642    // `test-dependency`'s.
1643    let Some(dependencies) = direct_dependencies(
1644        package,
1645        package
1646            .manifest
1647            .dependencies
1648            .iter()
1649            .chain(package.manifest.test_dependencies.iter()),
1650        build,
1651        published,
1652        errors,
1653    ) else {
1654        return Vec::new();
1655    };
1656
1657    // The map is built again over both roots and both dependency maps. `compile_in_build`
1658    // already reported every collision among the two roots and the plain dependencies,
1659    // before `src/` was checked, and stopped there if it found one; what this can still
1660    // find is a collision with a `test-dependency`'s module, which could not be seen
1661    // until that package was compiled.
1662    let visible = match resolve::visible_modules(package, &local_modules, &dependencies) {
1663        Ok(visible) => visible,
1664        Err(collisions) => {
1665            errors.push(CompilationError::Resolution(collisions));
1666            return Vec::new();
1667        }
1668    };
1669
1670    // A `test-dependency`'s modules are available to `tests/` and to nothing else, so
1671    // they join the environment here and never in `compile_in_build`. The plain
1672    // dependencies' are already in it, from `src/`.
1673    let test_dependencies: std::collections::HashSet<&PackageName> =
1674        package.manifest.test_dependencies.keys().collect();
1675    insert_dependency_interfaces(
1676        package,
1677        &visible,
1678        published,
1679        |origin| test_dependencies.contains(origin),
1680        &mut interfaces,
1681    );
1682
1683    let RootCheck {
1684        checked: can_mods,
1685        failing: failing_mods,
1686    } = check_root(
1687        package,
1688        source::SourceRoot::Tests,
1689        &modules,
1690        &module_files,
1691        &mut interfaces,
1692        errors,
1693        status,
1694    );
1695
1696    // A facade under `tests/` places its companion the way any other facade does — beside
1697    // its own source — under `tests/` here rather than `src/`
1698    // (`docs/spec/interop.md#testing-a-companion`).
1699    let root_dir = package.root.join(source::SourceRoot::Tests.directory());
1700    failing.extend(to_checked_sources(failing_mods, &root_dir, &module_files));
1701    to_checked_sources(can_mods, &root_dir, &module_files)
1702}
1703
1704/// The direct dependencies named by `entries`, each as the package being compiled sees
1705/// it, sorted by name so that a build reports two broken entries in the same order
1706/// every run.
1707///
1708/// `None` when one of them is in the build and did not compile — resolution succeeded,
1709/// or we would not be here. Its own diagnostics say why; the
1710/// [`DependencyNotCompiled`](resolve::Error::DependencyNotCompiled) this pushes says
1711/// which package was left uncompiled because of it, so a user reading a wall of errors
1712/// from a dependency knows why nothing was said about the package they asked for.
1713///
1714/// No status line goes with it. A status line reports a phase this package got through;
1715/// a package abandoned before its first phase has none, and the diagnostic already says
1716/// the same sentence in the same words.
1717fn direct_dependencies<'b, 'e>(
1718    package: &resolve::ResolvedPackage,
1719    entries: impl Iterator<Item = (&'e PackageName, &'e manifest::Dependency)>,
1720    build: &'b [resolve::ResolvedPackage],
1721    published: &HashMap<PackageName, HashMap<Name, Interface>>,
1722    errors: &mut Vec<CompilationError>,
1723) -> Option<Vec<resolve::DependencyModules<'b>>> {
1724    let mut entries: Vec<(&PackageName, &manifest::Dependency)> = entries.collect();
1725    entries.sort_by(|left, right| left.0.as_str().cmp(right.0.as_str()));
1726
1727    let mut dependencies: Vec<resolve::DependencyModules<'b>> = Vec::new();
1728    for (name, entry) in entries {
1729        let resolved = build.iter().find(|candidate| &candidate.name == name);
1730        let public = published.get(name);
1731
1732        match (resolved, public) {
1733            (Some(resolved), Some(public)) => dependencies.push(resolve::DependencyModules {
1734                package: resolved,
1735                wrapped: entry.wrapped,
1736                modules: public.keys().cloned().collect(),
1737            }),
1738            _ => {
1739                errors.push(CompilationError::Resolution(vec![
1740                    resolve::Error::DependencyNotCompiled {
1741                        package: package.name.clone(),
1742                        dependency: name.clone(),
1743                    },
1744                ]));
1745                return None;
1746            }
1747        }
1748    }
1749
1750    Some(dependencies)
1751}
1752
1753/// Add to `interfaces` every module of `visible` that belongs to a dependency whose name
1754/// `include` accepts, under the spelling `visible` keys it by.
1755///
1756/// This package's own modules are skipped: `check_root` inserts each one as it is
1757/// checked, under the name it declares.
1758fn insert_dependency_interfaces(
1759    package: &resolve::ResolvedPackage,
1760    visible: &HashMap<Name, resolve::ModuleOrigin>,
1761    published: &HashMap<PackageName, HashMap<Name, Interface>>,
1762    include: impl Fn(&PackageName) -> bool,
1763    interfaces: &mut HashMap<Name, Interface>,
1764) {
1765    for (spelling, origin) in visible {
1766        if origin.package == package.name || !include(&origin.package) {
1767            continue;
1768        }
1769
1770        if let Some(interface) = published
1771            .get(&origin.package)
1772            .and_then(|modules| modules.get(&origin.module))
1773        {
1774            interfaces.insert(spelling.clone(), interface.clone());
1775        }
1776    }
1777}
1778
1779/// The modules of one source root as the parser left them.
1780struct ParsedRoot {
1781    modules: Vec<parser::Module>,
1782    /// Which file each module was parsed from. This is the only point in the compiler
1783    /// where both halves are in scope at once — a phase is handed a module and never
1784    /// learns where it came from — so the mapping is recorded here and used by
1785    /// `check_root` to wrap the check errors in `InFile`, which is what lets their labels
1786    /// render.
1787    module_files: HashMap<Name, SourceFileId>,
1788    /// Every module the root declares, with the file that declared it. A name two files
1789    /// both answer to loses one of them in `module_files`, so the collision check is
1790    /// given this list instead.
1791    local_modules: Vec<resolve::LocalModule>,
1792    /// How many files failed to parse, each already reported. A file counts once
1793    /// however many of its declarations failed.
1794    failures: usize,
1795    /// Every file whose module header did not parse, under the module name its path
1796    /// gives it ([`source::files::SourceFile::module_name`]). Such a file contributes no
1797    /// module, and these are the names it is still known by.
1798    headless: Vec<(Name, SourceFileId)>,
1799}
1800
1801/// Parse every file of `ids`, pushing every syntax error of each file that has any onto
1802/// `errors`.
1803///
1804/// A file is parsed past its first error (`parser::parse_recovering`), so each of its
1805/// failing declarations is reported. It counts once in `failures` however many it has.
1806/// Its module is kept whenever the header parsed, holding every declaration that parsed
1807/// and a record of each that did not ([`parser::Module::failed`]), which canonicalization
1808/// reads as broken or as making the scope incomplete. A file whose header did not parse
1809/// contributes no module, and is listed in `headless` instead.
1810fn parse_root(
1811    ids: &[SourceFileId],
1812    sources: &SourceFiles,
1813    errors: &mut Vec<CompilationError>,
1814) -> ParsedRoot {
1815    let mut parsed = ParsedRoot {
1816        modules: vec![],
1817        module_files: HashMap::new(),
1818        local_modules: vec![],
1819        failures: 0,
1820        headless: vec![],
1821    };
1822
1823    for (id, file) in sources.iter().filter(|(id, _)| ids.contains(id)) {
1824        let parser::Parsed { module, failures } = parser::parse_recovering(file.file());
1825
1826        if !failures.is_empty() {
1827            parsed.failures += 1;
1828            errors.extend(
1829                failures
1830                    .into_iter()
1831                    .map(|failure| CompilationError::from(failure.error, id)),
1832            );
1833        }
1834
1835        match module {
1836            Some(module) => {
1837                parsed.module_files.insert(module.name.clone(), id);
1838                parsed.local_modules.push(resolve::LocalModule {
1839                    name: module.name.clone(),
1840                    file: file.package_path(),
1841                });
1842                parsed.modules.push(module);
1843            }
1844            None => parsed.headless.push((file.module_name(), id)),
1845        }
1846    }
1847
1848    parsed
1849}
1850
1851/// The status line for one root's parse: `parsed 8 modules`, or how many failed. The
1852/// first number is the files that parsed whole.
1853fn record_parse_status(parsed: &ParsedRoot, what: &str, status: &mut Vec<Status>) {
1854    let count = parsed
1855        .modules
1856        .iter()
1857        .filter(|module| module.failed.is_empty())
1858        .count();
1859    if parsed.failures == 0 {
1860        status.push(Status {
1861            success: true,
1862            text: format!("parsed {} {}", count, what),
1863        });
1864    } else {
1865        status.push(Status {
1866            success: false,
1867            text: format!(
1868                "parsed {} {}, {} failed to parse",
1869                count, what, parsed.failures
1870            ),
1871        });
1872    }
1873}
1874
1875/// The modules of one source root that [`check_root`] built a tree for.
1876struct RootCheck {
1877    /// The modules that checked.
1878    checked: Vec<CheckedModule>,
1879    /// The modules that came back with errors beside them.
1880    failing: Vec<CheckedModule>,
1881}
1882
1883/// Order the modules of one source root by their imports and check each one, against
1884/// `interfaces` and every module of the root checked before it.
1885///
1886/// Every module the check built a tree for is inserted into `interfaces` and handed
1887/// back, apart into the ones that checked and the ones that came back with errors; every
1888/// error goes onto `errors`, tagged with the file its module was read from.
1889///
1890/// A module is among the ones that checked when it parsed whole, its error list is empty
1891/// and [`canonical::Module::incomplete`] is false. The error list alone is not enough, for
1892/// three reasons that all end in a module with an empty list:
1893///
1894/// - a module with a declaration that failed to parse has that syntax error reported
1895///   apart, by `parse_root`, and canonicalization reports nothing more about the
1896///   declaration;
1897/// - a module whose every error was a not-found restating a failure of another module
1898///   has declarations with no IR;
1899/// - a module that imports an [incomplete](Interface::incomplete) interface was checked
1900///   in a scope where an error of its own could have been dropped, so it cannot be
1901///   claimed as checked even when its tree is whole and none of its declarations is
1902///   broken.
1903///
1904/// A module with a declaration in [`canonical::Module::broken`], or with a hole in one
1905/// of its declarations, is never among the ones that checked either, and needs no
1906/// condition of its own: a declaration is only recorded as broken, and a name is only a
1907/// hole, beside a syntax error, beside an error that is in the list, or beside one that
1908/// was dropped, and a dropped error is one raised in an incomplete scope.
1909fn check_root(
1910    package: &resolve::ResolvedPackage,
1911    root: source::SourceRoot,
1912    modules: &[parser::Module],
1913    module_files: &HashMap<Name, SourceFileId>,
1914    interfaces: &mut HashMap<Name, Interface>,
1915    errors: &mut Vec<CompilationError>,
1916    status: &mut Vec<Status>,
1917) -> RootCheck {
1918    let mut root_check = RootCheck {
1919        checked: Vec::new(),
1920        failing: Vec::new(),
1921    };
1922
1923    debug!("phase: Build module dependency graph ({})", root);
1924    // A cycle leaves us with no order to check the modules in, so the check phase is
1925    // skipped — but the error goes through the same reporting path as the others
1926    // instead of returning early unrendered.
1927    let walker =
1928        match dependencies::ModuleWalker::new_for_root(modules, module_files, &package.name) {
1929            Ok(walker) => walker,
1930            Err(err) => {
1931                errors.push(err.into());
1932                return root_check;
1933            }
1934        };
1935
1936    debug!("phase: Check modules ({})", root);
1937
1938    // Step 5: Follow graph and call check_module_recovering on each
1939    //
1940    // `check_in_order` checks every module regardless of earlier failures and hands back
1941    // one outcome per module (see `docs/tickets/README.md`, `BUG-2`).
1942    // `check_module_recovering` always answers `Outcome::Module`: the module with the
1943    // errors found in it, which is empty for a module that checked and for one whose errors
1944    // were all dropped in an incomplete scope. `Outcome::Failed` is the walker's answer
1945    // for a checker with nothing to publish, and only `dependencies.rs`'s own tests have
1946    // one; the arm below keeps the walker generic. The errors flow into `errors` below so
1947    // a failing module keeps making the build return `Err`.
1948    //
1949    // `module_files` is also how each module's `Interface` learns which file it came
1950    // from (`Interface::file`, `ERR-5`): this is the one place that knows both the
1951    // module and its file, so `check_in_order` takes the map and stamps it onto every
1952    // interface it inserts as it goes.
1953    let outcomes = walker.check_in_order(
1954        &package.name,
1955        interfaces,
1956        module_files,
1957        check_module_recovering,
1958    );
1959
1960    let unparsed: HashSet<&Name> = modules
1961        .iter()
1962        .filter(|module| !module.failed.is_empty())
1963        .map(|module| &module.name)
1964        .collect();
1965
1966    let mut check_errors = Vec::new();
1967    let mut failed = 0;
1968    for outcome in outcomes {
1969        match outcome {
1970            dependencies::Outcome::Module(module, module_errors)
1971                if module_errors.is_empty()
1972                    && !module.canonical.incomplete
1973                    && !unparsed.contains(module.canonical.name.name()) =>
1974            {
1975                root_check.checked.push(module)
1976            }
1977            dependencies::Outcome::Module(module, module_errors) => {
1978                failed += 1;
1979                root_check.failing.push(module);
1980                check_errors.extend(module_errors);
1981            }
1982            dependencies::Outcome::Failed(error) => {
1983                failed += 1;
1984                check_errors.push(error);
1985            }
1986        }
1987    }
1988
1989    let what = match root {
1990        source::SourceRoot::Src => "modules",
1991        source::SourceRoot::Tests => "test modules",
1992    };
1993    let names: Vec<String> = root_check
1994        .checked
1995        .iter()
1996        .map(|m| m.canonical.name.as_human_string())
1997        .collect();
1998
1999    if failed == 0 {
2000        status.push(Status {
2001            success: true,
2002            text: format!("checked {}: {:#?}", what, names),
2003        });
2004    } else {
2005        status.push(Status {
2006            success: false,
2007            text: format!(
2008                "checked {}: {:#?} ({} failed to check)",
2009                what, names, failed
2010            ),
2011        });
2012    }
2013
2014    // Tag each error with the file its module was read from, so the spans its phase
2015    // produced have something to point into. A module with no entry — there is none
2016    // today, since only a module that parsed can be checked — stays unwrapped and
2017    // renders exactly as it did before spans existed.
2018    errors.extend(check_errors.into_iter().map(|error| {
2019        match error.module().and_then(|name| module_files.get(name)) {
2020            Some(id) => CompilationError::InFile(Box::new(error), *id),
2021            None => error,
2022        }
2023    }));
2024
2025    root_check
2026}
2027
2028/// Take a parsed module file within the ecosystem and apply all checks to it
2029///
2030/// [`check_module_recovering`], reduced to a module that either checked or did not: a
2031/// module that came back with errors is `Err` with the first of them.
2032pub fn check_module(
2033    package: &PackageName,
2034    interfaces: &HashMap<Name, Interface>,
2035    source: &parser::Module,
2036) -> Result<CheckedModule, CompilationError> {
2037    match check_module_recovering(package, interfaces, source) {
2038        dependencies::Outcome::Failed(error) => Err(error),
2039        dependencies::Outcome::Module(module, errors) => match errors.into_iter().next() {
2040            None => Ok(module),
2041            Some(error) => Err(error),
2042        },
2043    }
2044}
2045
2046/// Apply every check to one parsed module, and hand back the module the checks built
2047/// beside everything they found wrong with it.
2048///
2049/// The module always comes back, as [`Outcome::Module`](dependencies::Outcome::Module):
2050/// an import that does not resolve is an error beside it and not a reason for there to
2051/// be none. It holds every declaration that canonicalized, whatever canonicalization, the
2052/// typer and the exhaustiveness check say, with a [`CompilationError::Canonical`], a
2053/// [`CompilationError::Type`] and a [`CompilationError::Exhaustiveness`] for whichever
2054/// of them reported anything. An empty error list does not say that the module checked
2055/// whole: a not-found error raised in an [incomplete](canonical::Module::incomplete)
2056/// scope is dropped, so a module can come back with no error and with declarations in
2057/// [`canonical::Module::broken`] or holding a hole; the driver reads the flag beside the
2058/// list.
2059///
2060/// Its IR holds a typed declaration for every value the typer did not reject, and lists
2061/// each rejected one, and each declaration canonicalization recorded in [`canonical::Module::broken`], in [`ir::Module::unchecked`] with
2062/// [`reported`](ir::Unchecked::reported) set, which no backend emits. A name that did not
2063/// resolve stands in a typed declaration as a [hole](ir::TypedTermKind::Hole), which no
2064/// backend emits either.
2065///
2066/// TODO canonicalization must happens before checkings, because type check (at least)
2067/// will require access to other modules canonical representation.
2068/// That probably mean moving the `canonical::canonicalize` call out of this function
2069///
2070/// Whether `source`'s package is exempt from the default imports is not decided
2071/// here: `canonical::canonicalize_recovering` derives it from `package` itself
2072/// ([`PackageName::is_core`]), so this function needs nothing beyond the package it
2073/// already receives — which is why `check` in
2074/// [`ModuleWalker::check_in_order`](dependencies::ModuleWalker::check_in_order) can
2075/// carry this as a plain `fn` pointer over the package alone.
2076pub fn check_module_recovering(
2077    package: &PackageName,
2078    interfaces: &HashMap<Name, Interface>,
2079    source: &parser::Module,
2080) -> dependencies::Outcome<CheckedModule, CompilationError> {
2081    // - desugar ~?~ *!*
2082    // Should I have an intermediate AST before type checking ?
2083    // This could actually be useful to have something optimized for
2084    // the type checker. It would also be something that can be used
2085    // as an information dump for dependencies (keep types solved as
2086    // a result and don't type checks those modules more than once).
2087    //
2088    // Each phase accumulates its own errors and hands back all of them; this is where
2089    // they are tagged with the module they came from, because a phase only ever sees
2090    // one module and has no reason to carry its name around.
2091    let canonical::Canonicalized {
2092        module: mut canonical,
2093        errors: canonical_errors,
2094    } = canonical::canonicalize_recovering(package, interfaces, source);
2095
2096    let mut errors = Vec::new();
2097    if !canonical_errors.is_empty() {
2098        errors.push(CompilationError::Canonical(
2099            canonical_errors,
2100            source.name.clone(),
2101        ));
2102    }
2103
2104    // - type checking and inference
2105    //
2106    // The typer answers with the types it solved — one `ir::Solved` per declaration,
2107    // carrying a type on every node of the ones it could type and saying why for the
2108    // ones it could not, the ones it rejected included. Not logged on the way out:
2109    // `infer_annotated` already dumps each declaration's term under `debug`, and
2110    // `typer::type_check_recovering` is `pub`, so a test that wants the map calls it
2111    // directly. It reads the same interfaces canonicalization resolved this module's
2112    // imports against, for the types of what they declare.
2113    let typer::TypeCheck {
2114        solved,
2115        instances,
2116        rejected_derivations,
2117        errors: type_errors,
2118    } = typer::type_check_recovering(&canonical, interfaces);
2119    // The class carries that a derivation of it was rejected into the interface, so a
2120    // module deriving an instance from it does not report the same mistake again.
2121    for class in rejected_derivations {
2122        if let Some(class) = canonical.classes.get_mut(&class) {
2123            class.signature.derivations_rejected = true;
2124        }
2125    }
2126    if !type_errors.is_empty() {
2127        errors.push(CompilationError::Type(type_errors, source.name.clone()));
2128    }
2129
2130    // verify in pattern matching branches that all variants are covered
2131    if let Err(exhaustiveness_errors) = exhaustiveness::check(&canonical) {
2132        errors.push(CompilationError::Exhaustiveness(
2133            exhaustiveness_errors,
2134            source.name.clone(),
2135        ));
2136    }
2137
2138    // - the shape a backend reads. Built here because this is the last frame holding
2139    // both the canonical module and what the typer solved from it, and nothing after
2140    // this point needs either half separately. Built whether or not anything above
2141    // reported an error, since a module with errors still has a typed tree for every
2142    // declaration the typer did not reject.
2143    let ir = ir::build(&canonical, solved, instances);
2144
2145    dependencies::Outcome::Module(CheckedModule { canonical, ir }, errors)
2146}
2147
2148/// One module the checks built a tree for, in both the forms the compiler still needs
2149/// it. Whether it passed them is said beside it: by [`check_module`]'s `Ok`, or by the
2150/// empty error list of [`check_module_recovering`]'s
2151/// [`Outcome::Module`](dependencies::Outcome::Module).
2152///
2153/// The two halves answer different questions and neither is derivable from the other.
2154/// [`canonical`](Self::canonical) is what an [`Interface`] is built from, so the modules
2155/// that import this one are checked against it; [`ir`](Self::ir) is what a backend
2156/// reads, and carries the solved types, the name kinds, the arities and the constructor
2157/// positions that emission needs and name resolution never did.
2158#[derive(Debug)]
2159pub struct CheckedModule {
2160    pub canonical: canonical::Module,
2161    pub ir: ir::Module,
2162}
2163
2164impl CheckedModule {
2165    /// This module's [`Interface`] — [`canonical::Module::to_interface`], which is the
2166    /// half of a checked module that crosses a module boundary.
2167    ///
2168    /// Inherent so that a caller holding a `CheckedModule` reaches it without importing
2169    /// [`Checked`]; the body is that trait's, so there is only one to keep correct.
2170    pub fn to_interface(&self, file: Option<SourceFileId>) -> Interface {
2171        Checked::to_interface(self, file)
2172    }
2173}
2174
2175/// What [`dependencies::ModuleWalker::check_in_order`] needs of whatever its checker
2176/// hands back: a name to key the module by, and the interface the modules after it are
2177/// checked against.
2178///
2179/// It is a trait because the walker drives more than one checker. The compiler's is
2180/// [`check_module_recovering`], which answers with a [`CheckedModule`]; `crates/zelkova-compiler/tests/spec.rs` drives the
2181/// same walker with a checker that only canonicalizes, because a spec example is judged
2182/// on the errors each phase reports and there is nothing to emit from it.
2183pub trait Checked {
2184    /// The module's own name, package included.
2185    fn name(&self) -> &ModuleName;
2186
2187    /// The trimmed-down view the modules that import this one are checked against.
2188    ///
2189    /// `file` is where this module was read from, when the caller knows it — see
2190    /// [`Interface::file`].
2191    fn to_interface(&self, file: Option<SourceFileId>) -> Interface;
2192}
2193
2194impl Checked for canonical::Module {
2195    fn name(&self) -> &ModuleName {
2196        &self.name
2197    }
2198
2199    fn to_interface(&self, file: Option<SourceFileId>) -> Interface {
2200        canonical::Module::to_interface(self, file)
2201    }
2202}
2203
2204impl Checked for CheckedModule {
2205    fn name(&self) -> &ModuleName {
2206        &self.canonical.name
2207    }
2208
2209    fn to_interface(&self, file: Option<SourceFileId>) -> Interface {
2210        self.canonical.to_interface(file)
2211    }
2212}
2213
2214#[cfg(test)]
2215mod tests {
2216    use super::*;
2217    use codespan_reporting::diagnostic::Severity;
2218
2219    /// Every clause of the package-name rule, accepted and rejected side by side.
2220    ///
2221    /// The hyphen half is the reason this table exists: a name is read left to right and
2222    /// each hyphen has to be followed by an ASCII lowercase letter, which is what keeps
2223    /// `acme-widgets` from colliding with a package called `acme` holding a `widgets`
2224    /// something. `Not_Legal`, the only rejection the pipeline tests reach, fails on its
2225    /// first character and never enters the loop at all.
2226    ///
2227    /// Mutation-checked by dropping the `is_ascii_lowercase()` guard on the character
2228    /// after a hyphen (`a-`, `a--b` and `a-1` go green) and, separately, by starting the
2229    /// first-character check from `is_ascii_alphanumeric()` (`1a` goes green).
2230    #[test]
2231    fn a_package_name_is_lowercase_ascii_with_hyphens_between_letters() {
2232        let cases = [
2233            ("a", true),
2234            ("core", true),
2235            ("zelkova-core", true),
2236            ("a-b-c", true),
2237            ("html2", true),
2238            ("a1-b2", true),
2239            ("", false),
2240            ("-a", false),
2241            ("a-", false),
2242            ("a--b", false),
2243            ("a-1", false),
2244            ("1a", false),
2245            ("Core", false),
2246            ("Not_Legal", false),
2247            ("with space", false),
2248            ("zelkøva", false),
2249        ];
2250
2251        for (name, legal) in cases {
2252            assert_eq!(
2253                is_legal_package_name(name),
2254                legal,
2255                "{:?} should {} a legal package name",
2256                name,
2257                if legal { "be" } else { "not be" }
2258            );
2259        }
2260    }
2261
2262    /// `PackageName::core` skips `PackageName::new`'s check, so the check is run here
2263    /// instead: the one package name the compiler builds on its own is a legal one.
2264    #[test]
2265    fn the_core_package_name_is_a_legal_one() {
2266        assert_eq!(
2267            PackageName::new(resolve::CORE_PACKAGE),
2268            Ok(PackageName::core())
2269        );
2270    }
2271
2272    /// Canonicalization failures are errors, and used to be rendered as warnings.
2273    ///
2274    /// The severity is part of how a failure reaches the user, so it is pinned
2275    /// here rather than left to the eye. Mutation-checked by putting
2276    /// `Diagnostic::warning()` back in the `Canonical` arm of `as_diagnostic`.
2277    #[test]
2278    fn canonical_errors_render_as_errors() {
2279        let error = CompilationError::Canonical(
2280            vec![canonical::Error::NoBindings(
2281                zelkova_syntax::position::NodeSpan::none(),
2282            )],
2283            "Test".into(),
2284        );
2285
2286        assert_eq!(error.as_diagnostic().severity, Severity::Error);
2287    }
2288
2289    /// Same as above for dependency errors — a module cycle is not a warning.
2290    ///
2291    /// Mutation-checked by putting `Diagnostic::warning()` back in the
2292    /// `DependenciesError` arm of `as_diagnostic`.
2293    #[test]
2294    fn dependency_errors_render_as_errors() {
2295        let error = CompilationError::DependenciesError(dependencies::Error::CycleDetected(vec![
2296            dependencies::Cycle {
2297                path: vec!["A".into(), "B".into()],
2298                others: vec![],
2299                edges: vec![],
2300            },
2301        ]));
2302
2303        assert_eq!(error.as_diagnostic().severity, Severity::Error);
2304    }
2305
2306    /// A dependency cycle names the modules in it, and names them as a loop.
2307    ///
2308    /// This arm used to say "Dependencies error messages are not implemented yet"
2309    /// with `format!("{:?}", err)` in a note, so the module names only ever reached
2310    /// the user inside a `Debug` dump. Mutation-checked by dropping the
2311    /// write-back-to-the-start in `dependencies::Error::notes`, which turns the
2312    /// trailing `-> A` assertion red.
2313    #[test]
2314    fn dependency_cycle_notes_spell_out_the_loop() {
2315        let error = CompilationError::DependenciesError(dependencies::Error::CycleDetected(vec![
2316            dependencies::Cycle {
2317                path: vec!["A".into(), "B".into()],
2318                others: vec![],
2319                edges: vec![],
2320            },
2321        ]));
2322
2323        let diagnostic = error.as_diagnostic();
2324
2325        assert_eq!(diagnostic.message, "1 circular dependency between modules");
2326        assert_eq!(diagnostic.notes, vec!["cycle: A -> B -> A".to_string()]);
2327    }
2328
2329    /// `ERR-2`: `exhaustiveness::Error` was `pub enum Error {}` — uninhabited — and
2330    /// `From<exhaustiveness::Error> for CompilationError` was `todo!()`. The
2331    /// conversion could not fire only because the error could not be built; the
2332    /// first error the real checker reported would have panicked the compiler.
2333    ///
2334    /// Nothing constructs this variant on a non-test path yet, so this test is what
2335    /// establishes the phase can report at all. Mutation-checked by collapsing
2336    /// `exhaustiveness::Error::message` to a constant string, which drops both names
2337    /// out of the headline and turns the message assertion red.
2338    #[test]
2339    fn exhaustiveness_errors_render_as_errors() {
2340        let error = CompilationError::Exhaustiveness(
2341            vec![exhaustiveness::Error::NonExhaustiveMatch {
2342                value: "describe".into(),
2343                tpe: "Shape".into(),
2344                missing: vec!["Square".into(), "Triangle".into()],
2345            }],
2346            "Test".into(),
2347        );
2348
2349        let diagnostic = error.as_diagnostic();
2350
2351        assert_eq!(diagnostic.severity, Severity::Error);
2352        assert_eq!(
2353            diagnostic.message,
2354            "[Test] the `case` expression in `describe` does not cover every variant of `Shape`"
2355        );
2356        assert_eq!(
2357            diagnostic.notes,
2358            vec!["no branch matches: Square, Triangle".to_string()]
2359        );
2360    }
2361
2362    /// A phase that reported several errors renders all of them, not just the first.
2363    ///
2364    /// `Diagnostic` has one headline, so a group is summarised and every message
2365    /// becomes a note. Mutation-checked by making `phase_diagnostic` render only
2366    /// `errors[0]`, which drops the second note.
2367    #[test]
2368    fn several_phase_errors_all_reach_the_notes() {
2369        let error = CompilationError::Canonical(
2370            vec![
2371                canonical::Error::NoBindings(zelkova_syntax::position::NodeSpan::none()),
2372                canonical::Error::TypeDeclared(
2373                    "Shape".into(),
2374                    zelkova_syntax::position::NodeSpan::none(),
2375                ),
2376            ],
2377            "Test".into(),
2378        );
2379
2380        let diagnostic = error.as_diagnostic();
2381
2382        assert_eq!(diagnostic.message, "[Test] 2 canonical errors");
2383        assert_eq!(diagnostic.notes.len(), 2, "notes: {:?}", diagnostic.notes);
2384        assert!(
2385            diagnostic.notes[1].contains("Shape"),
2386            "the second error must survive, got {:?}",
2387            diagnostic.notes
2388        );
2389    }
2390}