Skip to main content

stav/
check.rs

1//! Loading and type checking whole programs.
2//!
3//! [`check`] loads every file of a root through the host's [`Loader`],
4//! resolves imports and part files, resolves every name and type checks
5//! every declaration, expression and statement, and enforces the totality
6//! rules: no recursive functions, bindings or types, and no cycles of
7//! imports, part files or `self` dependencies. Nothing is evaluated.
8
9mod body;
10pub(crate) mod builtins;
11mod call;
12pub(crate) mod decls;
13mod expr;
14pub(crate) mod graph;
15mod host;
16mod load;
17pub(crate) mod model;
18mod pattern;
19mod schema;
20pub(crate) mod suggest;
21mod summary;
22mod totality;
23pub(crate) mod types;
24
25use std::collections::{BTreeMap, BTreeSet};
26use std::path::{Path, PathBuf};
27
28pub use host::{
29    Host, HostModule, ImplicitRefs, ImportRoot, LoadError, Loader, MemoryLoader, RelPath, RootSpec,
30    Source, SymbolType,
31};
32pub use load::{FileId, MAX_SOURCE_LEN};
33pub use schema::{SchemaField, SchemaIndex, SchemaStruct, SchemaSum, SchemaVariant};
34
35use self::decls::{FileScope, Global};
36use self::load::{Origin, Program, Role, file_kind};
37use self::model::{Fact, LocalId, SelfRead, Target, Unit, UnitId};
38use self::totality::{CallGraph, Node};
39use self::types::{StructId, Types};
40use crate::diagnostic::{Diagnostic, Diagnostics, FileDiagnostic, NamedSource, Severity};
41use crate::span::Span;
42use crate::syntax::SyntaxNode;
43
44/// A program that passed every check, ready to be evaluated.
45///
46/// It owns every source file of the root, the declarations with their
47/// resolved types, the type and meaning of every expression, and the
48/// warnings found on the way.
49#[derive(Debug)]
50pub struct Checked {
51    pub(crate) program: Program,
52    pub(crate) types: Types,
53    pub(crate) globals: Vec<Global>,
54    pub(crate) units: Vec<Unit>,
55    pub(crate) root: StructId,
56    pub(crate) root_ty: types::TypeId,
57    pub(crate) entries: Vec<UnitId>,
58    pub(crate) implicit_parts: Vec<UnitId>,
59    pub(crate) let_order: Vec<types::GlobalId>,
60    calls: CallGraph,
61    pub(crate) ref_targets: BTreeMap<StructId, Vec<model::Step>>,
62    warnings: Diagnostics,
63    inputs: BTreeMap<PathBuf, FileId>,
64    pub(crate) symbols: Vec<SymbolType>,
65    pub(crate) implicit_refs: Vec<ImplicitRefs>,
66    pub(crate) import_roots: Vec<ImportRoot>,
67}
68
69impl Checked {
70    /// Returns the warnings found while checking.
71    pub fn warnings(&self) -> &Diagnostics {
72        &self.warnings
73    }
74
75    /// Returns every file of the program with its display name, in the order
76    /// the files were loaded.
77    pub fn files(&self) -> impl Iterator<Item = (FileId, &str)> {
78        self.program
79            .files
80            .iter()
81            .enumerate()
82            .map(|(i, f)| (FileId::new(i), f.name.as_str()))
83    }
84
85    /// Returns the source text of a file.
86    pub fn source(&self, file: FileId) -> Option<&str> {
87        self.program
88            .files
89            .get(file.index())
90            .map(|f| f.parsed.tree.root().text())
91    }
92
93    /// Returns every source file the program was read from, with its exact
94    /// source text, once each and ordered by path.
95    ///
96    /// A file inside an import root is named by the import root joined with
97    /// its relative path, a standard module by `std/<name>`, a host module
98    /// by its import path such as `ns/target`, and a prelude file or
99    /// implicit part by the name the host gave it. The program depends only
100    /// on these texts and on the configuration of the host, so a host can
101    /// key a cache of evaluations by hashing both with a hash function of
102    /// its choice.
103    pub fn inputs(&self) -> impl Iterator<Item = (&Path, &str)> {
104        self.inputs
105            .iter()
106            .filter_map(|(path, &file)| self.source(file).map(|text| (path.as_path(), text)))
107    }
108
109    /// Returns the names and texts of the files, for diagnostics.
110    pub(crate) fn named_sources(&self) -> Vec<NamedSource> {
111        diagnostics(&self.program, Vec::new()).files
112    }
113
114    /// Returns whether a top-level `let` reads `self`.
115    pub(crate) fn let_reads(&self, g: types::GlobalId) -> bool {
116        matches!(
117            &self.globals[g.index()].kind,
118            decls::GlobalKind::Let(decls::LetGlobal {
119                state: decls::LetState::Done { reads, .. },
120                ..
121            }) if !reads.is_empty()
122        )
123    }
124
125    /// Returns the annotated type of a top-level `let`.
126    pub(crate) fn let_annotation(&self, g: types::GlobalId) -> Option<types::TypeId> {
127        match &self.globals[g.index()].kind {
128            decls::GlobalKind::Let(l) => l.annotation,
129            decls::GlobalKind::Fn(_) => None,
130        }
131    }
132
133    /// Returns whether a top-level declaration is a function.
134    pub(crate) fn is_fn(&self, g: types::GlobalId) -> bool {
135        matches!(self.globals[g.index()].kind, decls::GlobalKind::Fn(_))
136    }
137}
138
139/// Returns the path of every distinct source file of the program. The
140/// first file loaded from a path stands for it.
141fn inputs(program: &Program, spec: &RootSpec) -> BTreeMap<PathBuf, FileId> {
142    let mut out = BTreeMap::new();
143    for (i, file) in program.files.iter().enumerate() {
144        let path = match &file.origin {
145            Origin::Path { root, rel } => spec
146                .import_roots
147                .get(*root)
148                .map_or_else(|| rel.to_path(), |r| r.path().join(rel.to_path())),
149            _ => PathBuf::from(&file.name),
150        };
151        out.entry(path).or_insert(FileId::new(i));
152    }
153    out
154}
155
156/// Loads and type checks the root `root` of `host`.
157///
158/// Every file of the root is read through the host's loader, parsed and
159/// checked. No part of the program is evaluated.
160///
161/// # Errors
162///
163/// Returns every error found, together with the warnings, when a file
164/// cannot be loaded, has syntax errors, or does not type check. Once files
165/// load and parse, as many independent errors as possible are reported in
166/// one run.
167///
168/// # Examples
169///
170/// ```
171/// use stav::{Host, ImportRoot, MemoryLoader, RootSpec};
172///
173/// let loader = MemoryLoader::new().with_file(
174///     "app/main.stav",
175///     "type Level = debug | info\nstruct Root { level: Level = info }\nlevel = debgu\n",
176/// );
177/// let root = RootSpec {
178///     root_struct: "Root".to_owned(),
179///     entries: vec!["app/main.stav".into()],
180///     import_roots: vec![ImportRoot::new("app")],
181/// };
182/// let errors = stav::check(&Host::new(loader), &root).unwrap_err();
183/// let error = &errors.iter().next().unwrap().diagnostic;
184/// assert_eq!(error.code, "E0302");
185/// assert_eq!(error.help.as_deref(), Some("did you mean `debug`?"));
186/// ```
187pub fn check(host: &Host, root: &RootSpec) -> Result<Checked, Diagnostics> {
188    let (program, mut found) = load::load(host, root);
189    for (i, file) in program.files.iter().enumerate() {
190        let id = Some(FileId::new(i));
191        let syntax = file
192            .parsed
193            .diagnostics
194            .iter()
195            .cloned()
196            .chain(file.parsed.file().validate(file_kind(file.role)));
197        found.extend(syntax.map(|diagnostic| FileDiagnostic {
198            file: id,
199            diagnostic,
200        }));
201    }
202    if found
203        .iter()
204        .any(|d| d.diagnostic.severity == Severity::Error)
205    {
206        return Err(diagnostics(&program, found));
207    }
208    let (found, parts) = {
209        let mut checker = Checker::new(&program, host);
210        checker.run(root);
211        checker.finish()
212    };
213    match parts {
214        Some(parts) => {
215            let warnings = diagnostics(&program, found);
216            let inputs = inputs(&program, root);
217            let mut types = parts.types;
218            let root_ty = types.intern(types::Type::Struct(parts.root));
219            Ok(Checked {
220                program,
221                types,
222                root_ty,
223                globals: parts.globals,
224                units: parts.units,
225                root: parts.root,
226                entries: parts.entries,
227                implicit_parts: parts.implicit_parts,
228                let_order: parts.let_order,
229                calls: parts.calls,
230                ref_targets: parts.ref_targets,
231                warnings,
232                inputs,
233                symbols: host.symbols.clone(),
234                implicit_refs: host.implicit_refs.clone(),
235                import_roots: root.import_roots.clone(),
236            })
237        }
238        None => Err(diagnostics(&program, found)),
239    }
240}
241
242/// What a successful run of the checker produced.
243struct Parts {
244    types: Types,
245    globals: Vec<Global>,
246    units: Vec<Unit>,
247    root: StructId,
248    entries: Vec<UnitId>,
249    implicit_parts: Vec<UnitId>,
250    let_order: Vec<types::GlobalId>,
251    calls: CallGraph,
252    ref_targets: BTreeMap<StructId, Vec<model::Step>>,
253}
254
255fn diagnostics(program: &Program, items: Vec<FileDiagnostic>) -> Diagnostics {
256    Diagnostics {
257        files: program
258            .files
259            .iter()
260            .map(|f| NamedSource {
261                name: f.name.clone(),
262                text: f.parsed.tree.root().text().to_owned(),
263            })
264            .collect(),
265        items,
266    }
267}
268
269/// A binding in a lexical scope.
270#[derive(Debug, Clone, Copy)]
271enum Binding {
272    Local(LocalId),
273    /// A binding of a `let ... in` expression that is not visible yet,
274    /// because the reference is in its own value or an earlier one. Names
275    /// resolve past it, and it only explains a name that resolves to
276    /// nothing.
277    Pending,
278}
279
280/// Where `self` may be used, and what it refers to.
281#[derive(Debug, Clone)]
282enum SelfMode {
283    /// `self` is not allowed. The text says where the expression is, for
284    /// example "a function body".
285    Forbidden(&'static str),
286    /// In the default of a field of the given struct.
287    Default(StructId),
288    /// In a body.
289    Body(BodyCx),
290}
291
292/// The blocks around a statement of a body, outermost first.
293#[derive(Debug, Clone)]
294struct BodyCx {
295    /// Identifies the body: an entry file, a part file instance or a
296    /// `part { }` expression.
297    top: u32,
298    frames: Vec<Frame>,
299    graph: body::SelfGraph,
300}
301
302/// One block around a statement.
303#[derive(Debug, Clone)]
304struct Frame {
305    target: Target,
306    /// The keys from the top of the body to the block, which identify the
307    /// block in the `self` graph.
308    path: Vec<String>,
309}
310
311/// The lexical bindings in scope, innermost last.
312#[derive(Debug, Clone, Default)]
313struct Scope {
314    bindings: BTreeMap<String, Vec<Binding>>,
315    order: Vec<String>,
316}
317
318impl Scope {
319    /// Returns a mark that [`Scope::truncate`] returns to.
320    fn len(&self) -> usize {
321        self.order.len()
322    }
323
324    fn push(&mut self, (name, binding): (String, Binding)) {
325        self.bindings.entry(name.clone()).or_default().push(binding);
326        self.order.push(name);
327    }
328
329    /// Removes the bindings pushed since `mark`.
330    fn truncate(&mut self, mark: usize) {
331        while self.order.len() > mark {
332            let Some(name) = self.order.pop() else { break };
333            if let Some(stack) = self.bindings.get_mut(&name) {
334                stack.pop();
335                if stack.is_empty() {
336                    self.bindings.remove(&name);
337                }
338            }
339        }
340    }
341
342    /// Returns the innermost visible binding of `name`.
343    fn lookup(&self, name: &str) -> Option<LocalId> {
344        self.bindings.get(name)?.iter().rev().find_map(|b| match b {
345            Binding::Local(id) => Some(*id),
346            Binding::Pending => None,
347        })
348    }
349
350    /// Returns whether `name` is a binding of an enclosing `let ... in`
351    /// expression that is not visible yet.
352    fn is_pending(&self, name: &str) -> bool {
353        self.bindings
354            .get(name)
355            .is_some_and(|stack| stack.iter().any(|b| matches!(b, Binding::Pending)))
356    }
357
358    fn names(&self) -> impl Iterator<Item = &str> {
359        self.bindings.keys().map(String::as_str)
360    }
361}
362
363/// The context an expression is checked in.
364#[derive(Debug, Clone)]
365struct Cx {
366    unit: UnitId,
367    file: FileId,
368    scope: Scope,
369    self_mode: SelfMode,
370    /// The declaration whose body is being checked, for the call graph.
371    owner: Option<Node>,
372    /// The `self` reads of the expression being checked.
373    reads: Vec<SelfRead>,
374    /// References to top-level `let` declarations of this file that start
375    /// at or after this offset are forward references.
376    limit: Option<usize>,
377    /// Whether a map or struct literal checked next contributes per field,
378    /// so that it may leave required fields out.
379    partial: bool,
380}
381
382impl Cx {
383    fn new(unit: UnitId, file: FileId, self_mode: SelfMode, owner: Option<Node>) -> Self {
384        Self {
385            unit,
386            file,
387            scope: Scope::default(),
388            self_mode,
389            owner,
390            reads: Vec::new(),
391            limit: None,
392            partial: false,
393        }
394    }
395
396    fn lookup(&self, name: &str) -> Option<LocalId> {
397        self.scope.lookup(name)
398    }
399}
400
401/// The state of a checking run.
402struct Checker<'p> {
403    program: &'p Program,
404    host: &'p Host,
405    diagnostics: Vec<FileDiagnostic>,
406    seen: BTreeSet<(Option<FileId>, &'static str, Span, String)>,
407    types: Types,
408    scopes: Vec<FileScope>,
409    prelude: FileScope,
410    globals: Vec<Global>,
411    units: Vec<Unit>,
412    unit_keys: BTreeMap<(FileId, Option<StructId>), UnitId>,
413    root: Option<StructId>,
414    entries: Vec<UnitId>,
415    implicit_parts: Vec<UnitId>,
416    entry_tops: BTreeMap<FileId, u32>,
417    next_top: u32,
418    calls: CallGraph,
419    let_order: Vec<types::GlobalId>,
420    ref_targets: BTreeMap<StructId, Vec<model::Step>>,
421    ref_uses: Vec<(FileId, Span, StructId)>,
422    /// The paths of the target maps of every struct, once the declared
423    /// types are known.
424    target_maps: Option<BTreeMap<StructId, Vec<Vec<model::Step>>>>,
425    pending_predicates: Vec<decls::PendingPredicate<'p>>,
426    predicate_args: Vec<decls::PredicateArgs<'p>>,
427    /// The part file units created but not checked yet, with the file and
428    /// the struct they are a body of. Checking them from a queue instead of
429    /// at their `include` keeps long chains of part files off the stack.
430    parts: Vec<(UnitId, FileId, StructId)>,
431}
432
433impl<'p> Checker<'p> {
434    fn new(program: &'p Program, host: &'p Host) -> Self {
435        Self {
436            program,
437            host,
438            diagnostics: Vec::new(),
439            seen: BTreeSet::new(),
440            types: Types::new(),
441            scopes: (0..program.files.len())
442                .map(|_| FileScope::default())
443                .collect(),
444            prelude: FileScope::default(),
445            globals: Vec::new(),
446            units: Vec::new(),
447            unit_keys: BTreeMap::new(),
448            root: None,
449            entries: Vec::new(),
450            implicit_parts: Vec::new(),
451            entry_tops: BTreeMap::new(),
452            next_top: 0,
453            calls: CallGraph::default(),
454            let_order: Vec::new(),
455            ref_targets: BTreeMap::new(),
456            ref_uses: Vec::new(),
457            target_maps: None,
458            pending_predicates: Vec::new(),
459            predicate_args: Vec::new(),
460            parts: Vec::new(),
461        }
462    }
463
464    fn run(&mut self, spec: &RootSpec) {
465        self.declare();
466        self.resolve_declarations();
467        self.find_root(&spec.root_struct);
468        self.check_structure();
469        self.check_declaration_bodies();
470        if let Some(root) = self.root {
471            for &file in &self.program.implicit_parts {
472                let unit = self.part_unit(file, root);
473                self.implicit_parts.push(unit);
474            }
475            for &file in &self.program.entries {
476                let unit = self.entry_body(file, root);
477                self.entries.push(unit);
478            }
479        }
480        while !self.parts.is_empty() || !self.predicate_args.is_empty() {
481            self.check_parts();
482            self.check_predicate_args();
483        }
484        self.check_totality();
485    }
486
487    /// Returns the diagnostics, and what the evaluator needs when none of
488    /// them is an error.
489    fn finish(mut self) -> (Vec<FileDiagnostic>, Option<Parts>) {
490        self.diagnostics
491            .sort_by_key(|d| (d.file, d.diagnostic.span.start, d.diagnostic.span.end));
492        let errors = self
493            .diagnostics
494            .iter()
495            .any(|d| d.diagnostic.severity == Severity::Error);
496        let parts = match self.root {
497            Some(root) if !errors => Some(Parts {
498                types: self.types,
499                globals: self.globals,
500                units: self.units,
501                root,
502                entries: self.entries,
503                implicit_parts: self.implicit_parts,
504                let_order: self.let_order,
505                calls: self.calls,
506                ref_targets: self.ref_targets,
507            }),
508            _ => None,
509        };
510        (self.diagnostics, parts)
511    }
512
513    fn report(&mut self, file: Option<FileId>, diagnostic: Diagnostic) {
514        let key = (
515            file,
516            diagnostic.code,
517            diagnostic.span,
518            diagnostic.message.clone(),
519        );
520        if self.seen.insert(key) {
521            self.diagnostics.push(FileDiagnostic { file, diagnostic });
522        }
523    }
524
525    fn error(&mut self, file: FileId, diagnostic: Diagnostic) {
526        self.report(Some(file), diagnostic);
527    }
528
529    fn fact(&mut self, cx: &Cx, node: SyntaxNode<'_>, fact: Fact) {
530        if let Some(slot) = self
531            .units
532            .get_mut(cx.unit.index())
533            .and_then(|unit| unit.facts.get_mut(node.index()))
534        {
535            *slot = fact;
536        }
537    }
538
539    /// Returns the unit of a file that is not a part file, creating it on
540    /// first use.
541    fn file_unit(&mut self, file: FileId) -> UnitId {
542        self.unit(file, None).0
543    }
544
545    /// Returns the unit of `file` in the context `target`, and whether it
546    /// was just created.
547    fn unit(&mut self, file: FileId, target: Option<StructId>) -> (UnitId, bool) {
548        if let Some(&unit) = self.unit_keys.get(&(file, target)) {
549            return (unit, false);
550        }
551        let unit = UnitId::new(self.units.len());
552        let count = self.program.file(file).parsed.tree.node_count();
553        self.units.push(Unit {
554            file,
555            target,
556            facts: vec![Fact::None; count],
557            locals: Vec::new(),
558        });
559        self.unit_keys.insert((file, target), unit);
560        (unit, true)
561    }
562
563    fn new_top(&mut self) -> u32 {
564        self.next_top += 1;
565        self.next_top
566    }
567
568    fn role(&self, file: FileId) -> Role {
569        self.program.file(file).role
570    }
571}