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}