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}