Skip to main content

zelkova_compiler/
program.rs

1//! A program's entry point: the module a manifest's `main` names, held to
2//! [*Programs*](../../docs/spec/packages.md#programs).
3//!
4//! That section asks three things of a package whose manifest has `main`: the name is a
5//! module under `src/`, the module exposes a value called `main`, and that value has type
6//! `Task ()`. The first is about the manifest and has no place in any source to point at,
7//! so it is [`ManifestError::MainModuleNotFound`](super::manifest::ManifestError::MainModuleNotFound).
8//! The other two are about the module, and are this module's [`Error`].
9//!
10//! [`check`] reads a module that already checked. The type it compares is the one
11//! inference solved for `main` — [`ir::Declaration::tpe`](super::ir::Declaration::tpe) —
12//! or, for a `main` the typer left unchecked, the one its annotation declares. `Task` is
13//! recognised by the qualified name of its declaration in `zelkova-core`,
14//! never by spelling, so a union some other module calls `Task` is not it.
15//!
16//! # Which packages are checked
17//!
18//! Every package of the build whose manifest has `main`, not only the one the compiler was
19//! pointed at: `compile_in_build` runs the check, and it runs for each package of the
20//! build alike. A package is a program because its own manifest says
21//! so, and whether that manifest is right cannot depend on which package started the
22//! build: a dependency whose `main` names nothing is as broken as a root whose `main`
23//! names nothing, and would fail the moment it was compiled on its own. Checking it only
24//! as the root would make one package valid in one build and invalid in another.
25
26use std::collections::HashMap;
27
28use super::canonical::ExportType;
29use super::name::{Name, QualName};
30use super::typer::{canonical_type_to_typer_type, Type};
31use super::{CheckedModule, PhaseError, SpanLabel};
32use zelkova_syntax::position::NodeSpan;
33
34/// The name a program's entry point has in the module the manifest names.
35const MAIN: &str = "main";
36
37/// The name of `zelkova-core`'s module declaring `Task`, and of the type itself.
38const TASK: &str = "Task";
39
40/// What is wrong with the module a manifest's `main` names.
41#[derive(Debug)]
42pub enum Error {
43    /// The module exposes no value called `main`.
44    MainNotExposed {
45        /// The module's `exposing (...)`, which is what has to change.
46        exposing: NodeSpan,
47        /// Where the module declares `main` without exposing it. `None` when it declares
48        /// no `main` at all.
49        declared: Option<NodeSpan>,
50    },
51    /// The module exposes `main`, and its type is not `Task ()`.
52    MainNotTask {
53        /// The type `main` was judged to have: the one inference solved, or, when the typer
54        /// marked `main` unchecked rather than failing it, the one its annotation
55        /// declares. `None` only when the annotation cannot be read as a typer type, which
56        /// no annotation is today: `typer::canonical_type_to_typer_type` reads every
57        /// canonical type.
58        found: Option<Type>,
59        /// `main`'s annotation, which every exposed value has
60        /// ([*An exposed declaration must be
61        /// annotated*](../../docs/spec/types.md#an-exposed-declaration-must-be-annotated)).
62        annotation: NodeSpan,
63    },
64}
65
66/// Check that `module` is fit to be a program's entry point: it exposes `main`, and
67/// `main`'s solved type is `Task.Task ()` as `zelkova-core` declares it.
68pub fn check(module: &CheckedModule) -> Result<(), Error> {
69    let main = Name::new(MAIN);
70    let canonical = &module.canonical;
71
72    // `values` holds every declaration, exposed or not; only the header says which of
73    // them are reachable.
74    let declared = canonical.values.get(&main);
75    let exposed = canonical.exports.exposes(&main, &ExportType::Value);
76    let Some(value) = declared.filter(|_| exposed) else {
77        return Err(Error::MainNotExposed {
78            exposing: canonical.exposing_span,
79            declared: declared.map(|value| value.span()),
80        });
81    };
82
83    // An exposed `main` that the typer could not check is in `unchecked` rather than
84    // `declarations`, and has no solved type. Its annotation is judged instead: every
85    // exposed value has one, and canonicalization has already validated it. Emission
86    // reports the unchecked declaration itself.
87    let found = module
88        .ir
89        .declarations
90        .iter()
91        .find(|declaration| declaration.name == main)
92        .map(|declaration| declaration.tpe.clone())
93        .or_else(|| match value {
94            super::canonical::Value::TypedValue { tpe, .. } => {
95                canonical_type_to_typer_type(tpe, &mut HashMap::new(), &mut 0)
96            }
97            _ => None,
98        });
99
100    match found {
101        Some(tpe) if is_task_of_unit(&tpe) => Ok(()),
102        found => Err(Error::MainNotTask {
103            found,
104            annotation: match value {
105                super::canonical::Value::TypedValue {
106                    annotation_span, ..
107                } => *annotation_span,
108                other => other.span(),
109            },
110        }),
111    }
112}
113
114/// Whether `tpe` is exactly `Task.Task ()`, the `Task` `zelkova-core` declares.
115fn is_task_of_unit(tpe: &Type) -> bool {
116    matches!(tpe, Type::Adt(name, args) if is_core_task(name) && matches!(args.as_slice(), [Type::Unit]))
117}
118
119/// Whether `name` is the declaration of `Task` in `zelkova-core`'s `Task` module.
120fn is_core_task(name: &QualName) -> bool {
121    name.package().is_core()
122        && name.module_name().as_str() == TASK
123        && name.unqualified_name().as_str() == TASK
124}
125
126/// The union at the head of `tpe`, when it is spelled `Task` and is not `zelkova-core`'s.
127fn other_task(tpe: &Type) -> Option<&QualName> {
128    match tpe {
129        Type::Adt(name, _) if name.unqualified_name().as_str() == TASK && !is_core_task(name) => {
130            Some(name)
131        }
132        _ => None,
133    }
134}
135
136impl PhaseError for Error {
137    fn message(&self) -> String {
138        match self {
139            Error::MainNotExposed { .. } => {
140                "this module is the package's `main`, and it exposes no value called `main`"
141                    .to_string()
142            }
143            Error::MainNotTask {
144                found: Some(found), ..
145            } => match other_task(found) {
146                // Written out, because the two types print alike: `Task ()` either way.
147                Some(name) => format!(
148                    "`main` has type `{}` where `Task` is `{}`, and a program's `main` must \
149                     have type `Task ()` with `zelkova-core`'s `Task`",
150                    found,
151                    name.to_name()
152                ),
153                None => format!(
154                    "`main` has type `{}`, and a program's `main` must have type `Task ()`",
155                    found
156                ),
157            },
158            Error::MainNotTask { found: None, .. } => {
159                "the type of `main` could not be determined, and a program's `main` must have \
160                 type `Task ()`"
161                    .to_string()
162            }
163        }
164    }
165
166    fn notes(&self) -> Vec<String> {
167        match self {
168            Error::MainNotExposed { declared: None, .. } => vec![
169                "the manifest's `main` names this module, so it has to declare `main : Task ()` \
170                 and expose it"
171                    .to_string(),
172            ],
173            Error::MainNotExposed {
174                declared: Some(_), ..
175            } => vec!["add `main` to the module's `exposing` list".to_string()],
176            Error::MainNotTask { .. } => Vec::new(),
177        }
178    }
179
180    fn labels(&self) -> Vec<SpanLabel> {
181        let label = |span: &NodeSpan, message: &str, primary: bool| {
182            span.span().map(|span| SpanLabel {
183                span,
184                message: message.to_string(),
185                primary,
186                file: None,
187            })
188        };
189        match self {
190            Error::MainNotExposed { exposing, declared } => label(
191                exposing,
192                "`main` is not among what this module exposes",
193                true,
194            )
195            .into_iter()
196            .chain(
197                declared
198                    .as_ref()
199                    .and_then(|span| label(span, "`main` is declared here", false)),
200            )
201            .collect(),
202            Error::MainNotTask { found, annotation } => {
203                let message = match found {
204                    Some(found) => format!("`main` is declared as `{}`", found),
205                    None => "`main` is declared here".to_string(),
206                };
207                label(annotation, &message, true).into_iter().collect()
208            }
209        }
210    }
211}