Skip to main content

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}