zelkova_compiler/default_imports.rs
1//! The imports every module gets without writing them.
2//!
3//! Zelkova's standard library is reachable from a module that imports nothing:
4//! `Int`, `Bool`, `True`, `+` and `<|` resolve in a file whose only declaration is
5//! `x = 1 + 2`, and `Maybe`, `Just`, `List.map` and `String.length` resolve there
6//! too. [*The default
7//! imports*](../../docs/spec/modules.md#the-default-imports) is the rule; this
8//! module is the list it names, plus the two questions asked of that list.
9//!
10//! # Why the list lives here and not in canonicalization
11//!
12//! Two phases need it, for two halves of one behaviour.
13//! `canonical::environment::new_environment`
14//! turns each entry into a [`parser::Import`] and runs it through `process_import`
15//! ahead of the module's own, so a name arriving implicitly is indistinguishable
16//! from one written by hand. [`dependencies::ModuleWalker::new`](crate::dependencies::ModuleWalker::new)
17//! adds the matching edge to the import graph, because a module can only resolve
18//! against an [`Interface`] that already exists — so
19//! `Basics` has to be *checked* before the module that never named it, and nothing
20//! but the dependency graph decides that order.
21//!
22//! Neither phase owns the other, and a copy of the list in each would be two lists.
23//! So it sits beside them both, in a module that is the list and nothing else.
24//!
25//! # Where the cycle does not happen
26//!
27//! `Basics` cannot implicitly import `Basics`, and `Maybe` implicitly importing
28//! `Result` importing `Maybe` is exactly the loop
29//! [`dependencies`](crate::dependencies) exists to reject. What keeps the
30//! graph acyclic is a property of the *package*, not of any one entry: `zelkova-core`
31//! — the package the eight belong to — is the exception, and it is all-or-nothing.
32//! No module of it receives any of the eight, not the eight themselves and not the
33//! facades they are built from, so nothing in the package can ever close a loop
34//! through an implicit edge.
35//! [`PackageName::is_core`](crate::PackageName::is_core) is that question,
36//! asked of the package's own name rather than of its module names:
37//! [`resolve::visible_modules`](crate::resolve::visible_modules) rejects any
38//! package other than `zelkova-core` that declares one of the eight, before any module
39//! of it is compiled, so a package this is true for is `zelkova-core` by construction
40//! rather than merely by what it happens to contain. Every module of every other
41//! package receives all eight, whatever it imports and whatever imports it. [*The
42//! default imports*](../../docs/spec/modules.md#the-default-imports) is the rule in
43//! full, and [`DEC-17`](../../docs/decisions/dec-17.md) is why it is scoped to the
44//! package rather than judged from the import graph.
45//!
46//! `zelkova-core`'s own modules write every import they use — `Basics.zel`,
47//! `Maybe.zel`, `Result.zel` and `Bitwise.zel` already do — which is what makes the
48//! exception affordable: nothing in the package spends an entry it does not receive.
49//!
50//! [`implicit_imports`] and `dependencies::add_default_import_edges` are asked the
51//! same question rather than each deriving their own answer: both take an `is_core`
52//! argument that traces back to one `PackageName::is_core` call, so a package is
53//! exempt to `new_environment` exactly when it is to `ModuleWalker::new`.
54//!
55//! # An implicit import is never a diagnostic
56//!
57//! A synthesised import has no source text behind it, so its span is
58//! [`NodeSpan::none`] and any error raised from it would render with no caret — a
59//! message about an `import` line the user never wrote. [`implicit_imports`]
60//! therefore applies an entry only when the package genuinely provides what that
61//! entry asks for: the module has to have been checked already, and a `Maybe(..)`
62//! or `List` entry additionally has to find that type declared in the interface,
63//! and a `Maybe(..)` entry has to find its constructors exposed.
64//! Anything missing is left out, and the names it would have brought fail at the
65//! place they are *used*, with a caret under them.
66//!
67//! That is what makes the list land in stages without a second change: `List`
68//! and `Char` are `.ignored` files under `std/core/src` today, so their entries do
69//! nothing. Each starts working on the day its module compiles — `String`'s and
70//! `Task`'s already do, since `LANG-73` and `LANG-74`.
71
72use super::name::Name;
73use super::Interface;
74use std::collections::HashMap;
75use zelkova_syntax::parser;
76use zelkova_syntax::position::NodeSpan;
77
78/// What a default import makes available **unqualified**, beyond the qualified
79/// spelling that every import provides.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Unqualified {
82 /// `exposing (..)` — everything the module exposes.
83 Everything,
84 /// `exposing (<Module>)` — the type the module is named after, without its
85 /// constructors.
86 Type,
87 /// `exposing (<Module>(..))` — that type together with its constructors.
88 TypeAndVariants,
89 /// No `exposing` clause at all: the module's names are reachable only under
90 /// its own prefix.
91 Nothing,
92}
93
94/// One entry of the default import list.
95///
96/// [`Unqualified::Type`] and [`Unqualified::TypeAndVariants`] name no type of
97/// their own because every entry using them exposes a type spelled exactly like
98/// its module — `List` from `List`, `Maybe` from `Maybe`, `Result` from `Result`,
99/// `Task` from `Task`, `Char` from `Char`, `String` from `String`.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct DefaultImport {
102 pub module: &'static str,
103 pub unqualified: Unqualified,
104}
105
106/// The eight modules every Zelkova module behaves as though it began by importing.
107///
108/// This is [*The default
109/// imports*](../../docs/spec/modules.md#the-default-imports) verbatim, in the
110/// order the chapter writes them. `Maybe` and `Result` bring their constructors
111/// because matching on them is the ordinary way to use them; `List`, `Task`, `Char`
112/// and `String` come as bare types because their modules' functions read better
113/// qualified.
114pub const DEFAULT_IMPORTS: &[DefaultImport] = &[
115 DefaultImport {
116 module: "Basics",
117 unqualified: Unqualified::Everything,
118 },
119 DefaultImport {
120 module: "List",
121 unqualified: Unqualified::Type,
122 },
123 DefaultImport {
124 module: "Maybe",
125 unqualified: Unqualified::TypeAndVariants,
126 },
127 DefaultImport {
128 module: "Result",
129 unqualified: Unqualified::TypeAndVariants,
130 },
131 DefaultImport {
132 module: "Task",
133 unqualified: Unqualified::Type,
134 },
135 DefaultImport {
136 module: "Char",
137 unqualified: Unqualified::Type,
138 },
139 DefaultImport {
140 module: "String",
141 unqualified: Unqualified::Type,
142 },
143 DefaultImport {
144 module: "Tuple",
145 unqualified: Unqualified::Nothing,
146 },
147];
148
149impl DefaultImport {
150 /// This entry's module name.
151 pub fn name(&self) -> Name {
152 Name::new(self.module)
153 }
154
155 /// The `import` this entry stands for, as though it had been written at the
156 /// top of the file.
157 ///
158 /// The span is [`NodeSpan::none`]: there is no source text behind it, and a
159 /// diagnostic that would have underlined this line renders without a caret
160 /// rather than pointing at an arbitrary one.
161 pub fn to_import(&self) -> parser::Import {
162 let name = self.name();
163 let exposed = |privacy| {
164 parser::Exposing::Explicit(vec![parser::Exposed::bare(parser::ExposedKind::Upper(
165 name.clone(),
166 privacy,
167 ))])
168 };
169
170 let exposing = match self.unqualified {
171 Unqualified::Everything => parser::Exposing::Open,
172 Unqualified::Type => exposed(parser::Privacy::Private),
173 Unqualified::TypeAndVariants => exposed(parser::Privacy::Public),
174 // What the grammar builds for an `import` with no `exposing` clause.
175 Unqualified::Nothing => parser::Exposing::Explicit(vec![]),
176 };
177
178 parser::Import {
179 name,
180 alias: None,
181 exposing,
182 span: NodeSpan::none(),
183 }
184 }
185
186 /// Whether `interface` provides what this entry asks of it.
187 ///
188 /// An `exposing (Maybe(..))` entry against a module that declares no `Maybe`
189 /// would raise [`EnvError::UnionNotFound`](crate::canonical::environment::EnvError::UnionNotFound)
190 /// from an `import` nobody wrote. Rather than report that, the entry is
191 /// dropped — see this module's documentation.
192 ///
193 /// The test for a [`Unqualified::Type`] or [`Unqualified::TypeAndVariants`]
194 /// entry is that the interface declares a **union** of the module's own name.
195 /// That is what `Maybe`, `Result` and `List` are, and `Char` and `String`, whose
196 /// declarations in `std/core` are each a one-constructor union of their own name, as
197 /// is [`Task`](../../docs/spec/evaluation-semantics.md#effects). The chapter does
198 /// not oblige `Task` to be a union: if it were declared as anything else — a type
199 /// alias, or a type the compiler knows without a declaration — this entry would be
200 /// silently dropped and nothing here would go red.
201 ///
202 /// A [`Unqualified::TypeAndVariants`] entry also needs the module to expose that
203 /// union's constructors — `Maybe(..)` in its header, not a bare `Maybe` — since
204 /// asking for constructors a module withholds raises
205 /// [`EnvError::ConstructorsNotExposed`](crate::canonical::environment::EnvError::ConstructorsNotExposed),
206 /// which would be another diagnostic about an `import` nobody wrote.
207 fn satisfied_by(&self, interface: &Interface) -> bool {
208 let name = self.name();
209 match self.unqualified {
210 Unqualified::Everything | Unqualified::Nothing => true,
211 Unqualified::Type => interface.unions.contains_key(&name),
212 Unqualified::TypeAndVariants => {
213 interface.unions.contains_key(&name) && !interface.opaque_unions.contains(&name)
214 }
215 }
216 }
217}
218
219/// Whether `module` is one of the modules the default imports name.
220///
221/// Such a module receives none of them when its package is `zelkova-core`
222/// ([`implicit_imports`]) — the only package a module of this name may belong to,
223/// since [`resolve::visible_modules`](crate::resolve::visible_modules)
224/// rejects any other package that declares one (see this module's documentation).
225pub fn is_default(module: &Name) -> bool {
226 DEFAULT_IMPORTS
227 .iter()
228 .any(|default| default.module == module.as_str())
229}
230
231/// The imports a module gets without writing them, given what it *did* write and
232/// which interfaces are available to it.
233///
234/// Empty whenever `is_core` is set — every module of `zelkova-core` gets none of
235/// the eight, including the ones named on the list themselves. That is why this
236/// no longer takes the module's own name: a module named on the list can only
237/// belong to `zelkova-core`
238/// ([`resolve::visible_modules`](crate::resolve::visible_modules) rejects
239/// it everywhere else), so the caller's package-level answer already covers it and
240/// there is nothing left for a per-module check to add. Otherwise this returns one
241/// [`parser::Import`] per entry that `written` does not already name and that
242/// `interfaces` can satisfy — a written `import` of a default module **replaces**
243/// the implicit one, so `import Maybe as M` means `M.map` and nothing else.
244pub fn implicit_imports(
245 written: &[parser::Import],
246 interfaces: &HashMap<Name, Interface>,
247 is_core: bool,
248) -> Vec<parser::Import> {
249 if is_core {
250 return Vec::new();
251 }
252
253 DEFAULT_IMPORTS
254 .iter()
255 .filter(|default| {
256 let name = default.name();
257 if written.iter().any(|import| import.name == name) {
258 return false;
259 }
260 interfaces
261 .get(&name)
262 .is_some_and(|interface| default.satisfied_by(interface))
263 })
264 .map(DefaultImport::to_import)
265 .collect()
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::canonical::{Type, TypeConstructor, UnionType};
272 use crate::name::QualName;
273 use crate::{ModuleName, PackageName};
274
275 /// An interface named `module`, declaring a union of the same name when
276 /// `with_union` — which is what a `Maybe(..)`-shaped entry needs to find.
277 fn interface(module: &str, with_union: bool) -> (Name, Interface) {
278 let mut unions = HashMap::new();
279 if with_union {
280 unions.insert(
281 Name::new(module),
282 UnionType {
283 span: NodeSpan::none(),
284 variables: vec![],
285 variants: vec![TypeConstructor {
286 name: "Just".into(),
287 type_parameters: vec![Type::Variable("a".into())],
288 // The union shares its module's name, so the declaration
289 // it names is `Maybe.Maybe`.
290 tpe: QualName::in_module(PackageName::core(), module, module),
291 }],
292 },
293 );
294 }
295
296 (
297 Name::new(module),
298 Interface {
299 module_name: ModuleName::new(
300 PackageName::new("zelkova-core").unwrap(),
301 Name::new(module),
302 ),
303 values: HashMap::new(),
304 unions,
305 opaque_unions: Default::default(),
306 infixes: HashMap::new(),
307 infix_functions: HashMap::new(),
308 arities: HashMap::new(),
309 classes: HashMap::new(),
310 instances: Vec::new(),
311 file: None,
312 incomplete: false,
313 },
314 )
315 }
316
317 fn interfaces(entries: Vec<(Name, Interface)>) -> HashMap<Name, Interface> {
318 entries.into_iter().collect()
319 }
320
321 fn names(imports: &[parser::Import]) -> Vec<String> {
322 imports.iter().map(|i| i.name.to_string()).collect()
323 }
324
325 /// Only the entries the package actually provides are applied.
326 ///
327 /// Mutation-checked by dropping the `interfaces.get(..)` filter in
328 /// `implicit_imports`: every one of the eight comes back instead of the two.
329 #[test]
330 fn only_available_modules_are_implicitly_imported() {
331 let available = interfaces(vec![interface("Basics", false), interface("Tuple", false)]);
332
333 assert_eq!(
334 names(&implicit_imports(&[], &available, false)),
335 vec!["Basics".to_string(), "Tuple".to_string()]
336 );
337 }
338
339 /// `LANG-57`: a module of a package that is `zelkova-core` gets none of the
340 /// eight — not only the modules named on the list, but every module beside
341 /// them too — while an otherwise identical module of a package that is not
342 /// `zelkova-core` still gets everything it can satisfy.
343 ///
344 /// Mutation-checked by dropping the `is_core` guard in `implicit_imports`: the
345 /// `true` case then comes back with `Basics` (and anything else `available` can
346 /// satisfy) instead of nothing.
347 #[test]
348 fn a_core_package_gets_no_implicit_imports() {
349 let available = interfaces(vec![
350 interface("Basics", false),
351 interface("Maybe", true),
352 interface("Result", true),
353 ]);
354
355 assert!(implicit_imports(&[], &available, true).is_empty());
356 assert!(!implicit_imports(&[], &available, false).is_empty());
357 }
358
359 /// A written import of a default module replaces the implicit one, so the two
360 /// cannot both register the same names and turn every use into an
361 /// `AmbiguousVariables`.
362 ///
363 /// Mutation-checked by dropping the `written.iter().any(..)` filter: `Basics`
364 /// comes back alongside the one the module wrote.
365 #[test]
366 fn a_written_import_replaces_the_implicit_one() {
367 let available = interfaces(vec![interface("Basics", false), interface("Tuple", false)]);
368 let written = vec![parser::Import {
369 name: "Basics".into(),
370 alias: None,
371 exposing: parser::Exposing::Open,
372 span: NodeSpan::none(),
373 }];
374
375 assert_eq!(
376 names(&implicit_imports(&written, &available, false)),
377 vec!["Tuple".to_string()]
378 );
379 }
380
381 /// A module that happens to be named `Maybe` but declares no `Maybe` type
382 /// cannot satisfy `exposing (Maybe(..))`, and the entry is dropped rather than
383 /// reported from an `import` line nobody wrote.
384 ///
385 /// Mutation-checked by making `satisfied_by` return `true` unconditionally:
386 /// `Maybe` reappears in the list, and `process_import` would then raise a
387 /// caret-less `UnionNotFound`.
388 #[test]
389 fn an_entry_the_module_cannot_satisfy_is_dropped() {
390 let with_type = interfaces(vec![interface("Maybe", true)]);
391 let without_type = interfaces(vec![interface("Maybe", false)]);
392
393 assert_eq!(
394 names(&implicit_imports(&[], &with_type, false)),
395 vec!["Maybe".to_string()]
396 );
397 assert!(implicit_imports(&[], &without_type, false).is_empty());
398 }
399
400 /// A `Maybe(..)`-shaped entry against a module exposing `Maybe` opaquely is
401 /// dropped too, since `process_import` would reject the constructors it asks
402 /// for. A bare-type entry asks for none, so the same opacity leaves `List`'s
403 /// applied.
404 ///
405 /// Mutation-checked by dropping the `opaque_unions` test from the
406 /// `TypeAndVariants` arm of `satisfied_by`: `Maybe` comes back alongside `List`.
407 #[test]
408 fn a_constructor_entry_against_an_opaque_type_is_dropped() {
409 let opaque = |module: &str| {
410 let (name, mut interface) = interface(module, true);
411 interface.opaque_unions.insert(name.clone());
412 (name, interface)
413 };
414 let available = interfaces(vec![opaque("Maybe"), opaque("List")]);
415
416 assert_eq!(
417 names(&implicit_imports(&[], &available, false)),
418 vec!["List".to_string()]
419 );
420 }
421
422 /// The shape each entry is turned into: `Basics` open, `Maybe` with its
423 /// constructors, `List` without, `Tuple` qualified only.
424 #[test]
425 fn each_entry_builds_the_import_the_chapter_writes() {
426 let of = |module: &str| {
427 DEFAULT_IMPORTS
428 .iter()
429 .find(|d| d.module == module)
430 .unwrap_or_else(|| panic!("`{}` is on the default import list", module))
431 .to_import()
432 };
433
434 assert_eq!(of("Basics").exposing, parser::Exposing::Open);
435 assert_eq!(
436 of("Maybe").exposing,
437 parser::Exposing::Explicit(vec![parser::Exposed::bare(parser::ExposedKind::Upper(
438 "Maybe".into(),
439 parser::Privacy::Public
440 ))])
441 );
442 assert_eq!(
443 of("List").exposing,
444 parser::Exposing::Explicit(vec![parser::Exposed::bare(parser::ExposedKind::Upper(
445 "List".into(),
446 parser::Privacy::Private
447 ))])
448 );
449 assert_eq!(of("Tuple").exposing, parser::Exposing::Explicit(vec![]));
450 assert!(of("Tuple").alias.is_none());
451 // A synthesised import has no source text behind it.
452 assert!(of("Tuple").span.span().is_none());
453 }
454}