Skip to main content

cove_sema/
resolve.rs

1//! Name resolution across the units of a module, and across the modules of a
2//! package.
3//!
4//! Resolution produces the flat program the runtime executes and the derived
5//! facts (`export` visibility, required capabilities, trait conformances)
6//! that tooling reports.
7//!
8//! ADR 0005 makes a module able to name another module's exported
9//! declarations, so resolution is a package-wide pass rather than a
10//! per-module one: a `use` is checked against another module's declarations,
11//! the module dependency graph must be acyclic, and required capabilities are
12//! derived from the whole package's call graph.
13//!
14//! # What a derived capability set promises
15//!
16//! ADR 0015: a derived set is a *lower bound*. Function types carry no latent
17//! capability set, so a call through a function value, a `dyn Trait`
18//! receiver, or a generic parameter's bound is one the call graph cannot
19//! follow to a declaration. Rather than pretend otherwise, resolution records
20//! *why* it could not — [`FnEntry::open_calls`] — and propagates that along
21//! the same edges the capabilities travel. A function that carries no open
22//! call has a complete set; one that does has a floor and says so, and the
23//! runtime's grant check remains the only thing that decides what a call may
24//! actually do.
25//!
26//! # Conformances
27//!
28//! An `impl Trait for Type` block is checked here and then flattened: every
29//! method it supplies, and every method the trait defaults that it does not
30//! override, is recorded as an ordinary method of the type. Dispatch never
31//! has to ask where a method came from, and a trait method that collides with
32//! an inherent method of the same name is caught by the same duplicate check
33//! that catches two inherent methods.
34//!
35//! The conformance itself is recorded separately, because the set of a
36//! trait's implementors is a fact the type checker needs (to check a bound)
37//! and tooling needs (to show a type's interface).
38//!
39//! Either party to a conformance may be imported (ADR 0006's orphan rule
40//! names the module that declares the trait or the module that declares the
41//! type; ADR 0005 lets a third module name both), so the rule is checked
42//! against what the module *declares*, not against what it can see. The two
43//! rules together also make a conformance unique without a further check:
44//! for both parties' modules to declare the same one, each would have to
45//! import the other, which is the cycle ADR 0005 forbids.
46
47use std::collections::{BTreeMap, BTreeSet};
48use std::sync::Arc;
49
50use cove_diag::{Diagnostic, Span, Spanned};
51use cove_schema::HostSchemas;
52use cove_syntax::ast::{
53    Block, EnumDecl, Expr, ExprKind, FnDecl, Item, ItemKind, MatchArm, Pattern, PatternKind,
54    Receiver, Stmt, StmtKind, StrPart, StructDecl, TraitDecl, TraitMethod, TypeAlias,
55};
56
57use crate::capability::{Capability, OpenCall};
58use crate::facts::Facts;
59use crate::package::Package;
60
61/// A declaration that belongs to a module, with the facts derived from it.
62#[derive(Debug)]
63pub struct FnEntry {
64    pub decl: Arc<FnDecl>,
65    pub exported: bool,
66    /// `test fn`: a declaration only the test runner calls.
67    ///
68    /// A test is module-private by construction — `export` and `test` cannot
69    /// both apply — so it sees its module's private declarations, and its
70    /// required capabilities are derived from its call graph exactly as any
71    /// other function's are.
72    pub is_test: bool,
73    pub doc: Option<String>,
74    /// The type this function is a method of, when it came from an `impl`.
75    pub receiver_type: Option<String>,
76    /// The trait whose default body this method runs, when the conformance
77    /// did not supply one of its own.
78    ///
79    /// Such a method's body belongs to the trait, not to this type, so it is
80    /// checked once where the trait declares it rather than once per
81    /// conformance.
82    pub from_trait_default: Option<String>,
83    /// Capabilities used directly in this function's body.
84    pub direct_capabilities: BTreeSet<Capability>,
85    /// Capabilities this function requires, including those reached through
86    /// calls to other declarations of the package — its own module's, and
87    /// any module it imports from.
88    ///
89    /// A call through a field access (`receiver.method(...)`) whose receiver
90    /// is not a bare reference to a struct or enum visible where the call is
91    /// written is resolved to *every* method sharing that name in this
92    /// module and in every module it imports from. There is no static type
93    /// checker yet to narrow the receiver's actual type, so this is a
94    /// deliberate over-approximation: it can report a capability a function
95    /// does not really need, but never omits one it does. Static type
96    /// checking would let this be exact.
97    ///
98    /// Module imports made no part of that precise. They widened it: an
99    /// unknown receiver used to be able to reach only the module's own
100    /// methods, and can now reach the methods of every module reachable
101    /// through its imports, because that is where a value it did not declare
102    /// can come from.
103    /// What they did narrow is the *other* direction — a call that leaves
104    /// the module is now followed rather than lost, so a capability reached
105    /// through an imported helper is reported instead of missed.
106    ///
107    /// This set is a *lower bound* when [`FnEntry::open_calls`] is not empty;
108    /// see ADR 0015.
109    pub required_capabilities: BTreeSet<Capability>,
110    /// The indirect calls written in this function's own body: calls the
111    /// call graph cannot follow, so whatever they reach is not in
112    /// `direct_capabilities`.
113    pub direct_open_calls: BTreeSet<OpenCall>,
114    /// Why `required_capabilities` is a lower bound rather than the whole of
115    /// what calling this function can reach — empty when it is the whole of
116    /// it.
117    ///
118    /// This is `direct_open_calls` propagated over the same call graph the
119    /// capabilities travel: a caller of a capability-open declaration is
120    /// capability-open too, because the requirement it cannot see is one its
121    /// own callers cannot see either.
122    pub open_calls: BTreeSet<OpenCall>,
123}
124
125impl FnEntry {
126    /// Whether [`FnEntry::required_capabilities`] is a lower bound: this
127    /// function, or something it calls, makes a call the call graph cannot
128    /// follow.
129    ///
130    /// Every report that shows a derived capability set asks this, so that
131    /// an incomplete set is never shown as though it were complete.
132    pub fn is_capability_open(&self) -> bool {
133        !self.open_calls.is_empty()
134    }
135}
136
137#[derive(Debug)]
138pub struct StructEntry {
139    pub decl: Arc<StructDecl>,
140    pub exported: bool,
141    /// `export opaque struct`: the export carries the type's name and its
142    /// exported methods, and no way to build or read one.
143    ///
144    /// The flag is recorded here rather than derived at each use because
145    /// every consumer asks the same question of it — the type checker, which
146    /// refuses a cross-module construction or field access, and `cove
147    /// outline` and `cove api snapshot`, which leave the representation out
148    /// of what they publish.
149    pub opaque: bool,
150    pub doc: Option<String>,
151}
152
153#[derive(Debug)]
154pub struct EnumEntry {
155    pub decl: Arc<EnumDecl>,
156    pub exported: bool,
157    pub doc: Option<String>,
158}
159
160/// A trait a module declares.
161#[derive(Debug)]
162pub struct TraitEntry {
163    pub decl: Arc<TraitDecl>,
164    pub exported: bool,
165    pub doc: Option<String>,
166}
167
168impl TraitEntry {
169    /// The method of this trait named `name`, if it declares one.
170    pub fn method(&self, name: &str) -> Option<&TraitMethod> {
171        self.decl.methods.iter().find(|m| m.name.node == name)
172    }
173}
174
175/// One `impl Trait for Type` block: the fact that `type_name` conforms to
176/// `trait_name`, and how.
177///
178/// Conformance is explicit, so this is the complete set of implementors a
179/// trait has, which is what makes a bound checkable and a `dyn Trait` value's
180/// implementation findable.
181#[derive(Clone, Debug)]
182pub struct Conformance {
183    pub trait_name: String,
184    pub type_name: String,
185    /// The module that declares the trait, and the module that declares the
186    /// type. Either may be this module or one it imports from — the orphan
187    /// rule only requires that one of them *is* this module — so a
188    /// conformance names both parties by the module they belong to.
189    pub trait_module: String,
190    pub type_module: String,
191    /// Every method the conformance supplies, whether written in the block or
192    /// inherited from the trait's default body.
193    pub methods: BTreeSet<String>,
194    /// The `impl Trait for Type` header, for a diagnostic to point at.
195    pub span: Span,
196}
197
198#[derive(Debug)]
199pub struct AliasEntry {
200    pub decl: Arc<TypeAlias>,
201    pub exported: bool,
202    pub doc: Option<String>,
203}
204
205/// Everything one module declares.
206#[derive(Debug, Default)]
207pub struct ResolvedModule {
208    pub name: String,
209    /// Free functions, keyed by name.
210    pub functions: BTreeMap<String, FnEntry>,
211    /// Methods and associated functions, keyed by `(type name, function name)`.
212    pub methods: BTreeMap<(String, String), FnEntry>,
213    pub structs: BTreeMap<String, StructEntry>,
214    pub enums: BTreeMap<String, EnumEntry>,
215    pub traits: BTreeMap<String, TraitEntry>,
216    /// Every declared conformance, keyed by `(trait name, type name)`.
217    pub conformances: BTreeMap<(String, String), Conformance>,
218    pub aliases: BTreeMap<String, AliasEntry>,
219    /// Host modules named by `use`, such as `console` from `use console.println`.
220    pub host_uses: BTreeSet<String>,
221    /// Names imported unqualified by `use`, such as `println` -> `console`.
222    pub host_items: BTreeMap<String, String>,
223    /// Declarations imported from another module of the package, mapping the
224    /// name they are visible under — the `use` path's last segment, which is
225    /// the declaration's own name — to the module that declares them.
226    ///
227    /// Only exported declarations ever appear here; a `use` naming a
228    /// module-private one is rejected.
229    pub imports: BTreeMap<String, String>,
230    /// Modules imported whole, mapping the name they are visible under — the
231    /// `use` path's last segment — to the full module name, so their exports
232    /// can be reached qualified as `booking.createBooking`.
233    pub module_imports: BTreeMap<String, String>,
234}
235
236impl ResolvedModule {
237    /// The module that declares `name` as this module sees it: itself when it
238    /// declares `name`, and the module `name` was imported from otherwise.
239    ///
240    /// A local declaration wins, which is only reachable when a conflicting
241    /// import was already reported: [`resolve`] refuses a `use` that binds a
242    /// name this module also declares.
243    pub fn owner_of<'a>(&'a self, name: &str) -> Option<&'a str> {
244        if self.functions.contains_key(name)
245            || self.structs.contains_key(name)
246            || self.enums.contains_key(name)
247            || self.traits.contains_key(name)
248            || self.aliases.contains_key(name)
249        {
250            return Some(&self.name);
251        }
252        self.imports.get(name).map(String::as_str)
253    }
254
255    /// Whether this module's declaration of `name` is exported, or `None`
256    /// when it declares no `name`.
257    ///
258    /// A method is not a declaration in this sense: it is reached through
259    /// its type, so the type's visibility is what governs it.
260    pub fn exported(&self, name: &str) -> Option<bool> {
261        if let Some(entry) = self.functions.get(name) {
262            return Some(entry.exported);
263        }
264        if let Some(entry) = self.structs.get(name) {
265            return Some(entry.exported);
266        }
267        if let Some(entry) = self.enums.get(name) {
268            return Some(entry.exported);
269        }
270        if let Some(entry) = self.traits.get(name) {
271            return Some(entry.exported);
272        }
273        self.aliases.get(name).map(|entry| entry.exported)
274    }
275
276    /// Every name this module exports, in name order.
277    pub fn exports(&self) -> Vec<String> {
278        let functions = self
279            .functions
280            .iter()
281            .filter(|(_, entry)| entry.exported)
282            .map(|(name, _)| name);
283        let structs = self
284            .structs
285            .iter()
286            .filter(|(_, entry)| entry.exported)
287            .map(|(name, _)| name);
288        let enums = self
289            .enums
290            .iter()
291            .filter(|(_, entry)| entry.exported)
292            .map(|(name, _)| name);
293        let traits = self
294            .traits
295            .iter()
296            .filter(|(_, entry)| entry.exported)
297            .map(|(name, _)| name);
298        let aliases = self
299            .aliases
300            .iter()
301            .filter(|(_, entry)| entry.exported)
302            .map(|(name, _)| name);
303        let mut names: Vec<String> = functions
304            .chain(structs)
305            .chain(enums)
306            .chain(traits)
307            .chain(aliases)
308            .cloned()
309            .collect();
310        names.sort();
311        names
312    }
313
314    /// Every module this one imports from, whether by declaration or whole.
315    pub fn dependencies(&self) -> BTreeSet<&str> {
316        self.imports
317            .values()
318            .chain(self.module_imports.values())
319            .map(String::as_str)
320            .collect()
321    }
322}
323
324/// A resolved package, ready to run or inspect.
325#[derive(Debug, Default)]
326pub struct Program {
327    pub modules: BTreeMap<String, ResolvedModule>,
328    /// Every diagnostic that does not stop the package: the resolver's own
329    /// warnings, such as a missing doc comment on an exported declaration,
330    /// and the type checker's warnings *and notes*.
331    ///
332    /// It is called `notices` rather than `warnings` because it holds two
333    /// severities and they ask for different things. A warning is a doubt,
334    /// and `cove check --deny-warnings` refuses a package that has one. A
335    /// note is not: it is the compiler naming something it deliberately did
336    /// not prove — a Host API result a schema declared `Any`, a variadic
337    /// operation used as a value — which no strictness setting can turn into
338    /// a proof. Every consumer therefore filters on the exact
339    /// [`cove_diag::Severity`] rather than on this field's length.
340    ///
341    /// It never holds an [`cove_diag::Severity::Error`]: an error is
342    /// returned as `Err` and the package does not resolve.
343    pub notices: Vec<Diagnostic>,
344    /// The package's call graph: for each declaration, every declaration it
345    /// may call, and how precisely the call site named it.
346    ///
347    /// This is the graph [`FnEntry::required_capabilities`] is the fixed
348    /// point over. It is kept rather than discarded because reachability
349    /// between declarations is a derived fact in its own right: it is what
350    /// answers which declarations a change can affect.
351    pub call_graph: BTreeMap<Node, BTreeMap<Node, CallPrecision>>,
352    /// What the type checker worked out about each expression: its type, and
353    /// for a call to a declared method, which declaration it chose.
354    ///
355    /// Resolution settles no types, so this is filled by the check rather
356    /// than here: [`Compiler::compile`](crate::compile::Compiler::compile)
357    /// runs both halves and puts the second one's answers here, and a
358    /// program that was only resolved carries none. ADR 0019 is why it is
359    /// carried at all — a pass that re-derives a fact the checker already
360    /// settled is a pass that can disagree with it.
361    pub facts: Facts,
362}
363
364impl Program {
365    /// Looks up a fully qualified entry such as `hello.main`.
366    pub fn lookup_fn(&self, module: &str, name: &str) -> Option<&FnEntry> {
367        self.modules.get(module)?.functions.get(name)
368    }
369
370    /// Every `test fn` the package declares, in module then name order.
371    ///
372    /// This is what `cove test` runs. A test is an ordinary declaration of
373    /// its module, so its required capabilities are already derived and its
374    /// body already checked; the runner only has to find it.
375    pub fn tests(&self) -> Vec<DeclaredTest<'_>> {
376        let mut found = Vec::new();
377        for (module, resolved) in &self.modules {
378            for (name, entry) in &resolved.functions {
379                if entry.is_test {
380                    found.push(DeclaredTest {
381                        module: module.as_str(),
382                        name: name.as_str(),
383                        entry,
384                    });
385                }
386            }
387        }
388        found
389    }
390
391    /// Every conformance declared for the type `type_module.type_name`,
392    /// paired with the module whose source declares it, in trait order.
393    ///
394    /// The declaring module is not always the type's own. ADR 0006's orphan
395    /// rule only requires that the module declaring an `impl Trait for Type`
396    /// block declares one of the two, so a conformance may be written where
397    /// the *trait* is. A type's conformances are therefore a fact about the
398    /// package, and asking one module for them under-reports the type's
399    /// interface.
400    pub fn conformances_of(&self, type_module: &str, type_name: &str) -> Vec<(&str, &Conformance)> {
401        let mut found: Vec<(&str, &Conformance)> = Vec::new();
402        for (module, resolved) in &self.modules {
403            for conformance in resolved.conformances.values() {
404                if conformance.type_module == type_module && conformance.type_name == type_name {
405                    found.push((module.as_str(), conformance));
406                }
407            }
408        }
409        found.sort_by(|(_, a), (_, b)| {
410            (&a.trait_module, &a.trait_name).cmp(&(&b.trait_module, &b.trait_name))
411        });
412        found
413    }
414
415    /// Every method of the type `type_module.type_name`, wherever it is
416    /// declared, in method-name order.
417    ///
418    /// A type's methods usually live in the module that declares the type. A
419    /// conformance is the exception, for the reason [`Self::conformances_of`]
420    /// gives: a method of this type can be declared by any module that
421    /// conforms it to a trait of its own.
422    ///
423    /// One name answers to one method, whichever module declares it:
424    /// `check_method_collisions` rejects a package where two modules
425    /// declare a method of one name for one type.
426    pub fn methods_of(&self, type_module: &str, type_name: &str) -> Vec<DeclaredMethod<'_>> {
427        let mut found: BTreeMap<&str, DeclaredMethod<'_>> = BTreeMap::new();
428        if let Some(owner) = self.modules.get(type_module) {
429            for ((owner_type, method), entry) in &owner.methods {
430                if owner_type == type_name {
431                    found.insert(
432                        method.as_str(),
433                        DeclaredMethod {
434                            module: owner.name.as_str(),
435                            name: method.as_str(),
436                            entry,
437                        },
438                    );
439                }
440            }
441        }
442        for (module, conformance) in self.conformances_of(type_module, type_name) {
443            let Some(owner) = self.modules.get(module) else {
444                continue;
445            };
446            for method in &conformance.methods {
447                let key = (type_name.to_string(), method.clone());
448                let Some(entry) = owner.methods.get(&key) else {
449                    continue;
450                };
451                found.insert(
452                    method.as_str(),
453                    DeclaredMethod {
454                        module: owner.name.as_str(),
455                        name: method.as_str(),
456                        entry,
457                    },
458                );
459            }
460        }
461        found.into_values().collect()
462    }
463}
464
465/// One `test fn`, and the module that declares it.
466#[derive(Clone, Copy, Debug)]
467pub struct DeclaredTest<'a> {
468    /// The module the test belongs to, whose private declarations it sees.
469    pub module: &'a str,
470    /// The test's own name.
471    pub name: &'a str,
472    /// The declaration itself, with the capabilities its call graph requires.
473    pub entry: &'a FnEntry,
474}
475
476impl DeclaredTest<'_> {
477    /// The name the runner reports and `--filter` matches, such as
478    /// `text.countsWords`.
479    pub fn qualified_name(&self) -> String {
480        format!("{}.{}", self.module, self.name)
481    }
482}
483
484/// One method of a type, and the module whose source declares it.
485#[derive(Clone, Copy, Debug)]
486pub struct DeclaredMethod<'a> {
487    /// The module that declares this method: the type's own, or one that
488    /// conforms the type to a trait of its own.
489    pub module: &'a str,
490    /// The method's name.
491    pub name: &'a str,
492    /// The method itself, with the facts derived from it.
493    pub entry: &'a FnEntry,
494}
495
496/// Resolves every module of `package` into the flat program the runtime
497/// executes.
498///
499/// Because a module may name another module's exported declarations, this is
500/// a package-wide pass rather than a per-module one:
501///
502/// 1. each module's *surface* — what it declares, and whether each
503///    declaration is exported — is collected, since a `use` in one module is
504///    answered by another module's declarations;
505/// 2. every `use` is resolved against the package's modules first and the
506///    host registry second (ADR 0005);
507/// 3. the module dependency graph is checked for cycles, which ADR 0005
508///    forbids;
509/// 4. each module's own declarations are merged across its units;
510/// 5. required capabilities, and the reasons they are only a lower bound,
511///    are derived as a fixed point over the *package's* call graph, so a
512///    function reaching a Host API through an imported helper reports it and
513///    a function reaching an indirect call through one says so;
514/// 6. every body is checked against everything now known, including enums
515///    reached through an import.
516pub fn resolve(package: &Package) -> Result<Program, Vec<Diagnostic>> {
517    resolve_with(package, &HostSchemas::new())
518}
519
520/// Resolves `package` against `schemas`, the host modules this compilation
521/// may name.
522///
523/// This is [`resolve`] with the one thing an embedder can change: the set of
524/// Host API descriptions the resolver reads. A module in `schemas` is a host
525/// module here in every sense a shipped one is -- it may not be shadowed by
526/// a package module, a `use` of it is not warned about, and a call into it
527/// requires the capability its own table declares.
528pub fn resolve_with(package: &Package, schemas: &HostSchemas) -> Result<Program, Vec<Diagnostic>> {
529    let mut program = Program::default();
530    let mut errors = Vec::new();
531    let mut warnings = Vec::new();
532
533    let surfaces: BTreeMap<&str, Surface> = package
534        .modules
535        .iter()
536        .map(|(name, module)| (name.as_str(), Surface::of(module)))
537        .collect();
538
539    let opaque_fields = OpaqueFields::of(package);
540
541    let mut call_sites: BTreeMap<Node, Vec<CallShape>> = BTreeMap::new();
542    let mut edges: Vec<ImportEdge> = Vec::new();
543    // Modules a warning has already been issued for, so a package where
544    // several `use`s (in one module or several) name the same undescribed
545    // host module is told once rather than once per `use`. `package.modules`
546    // is a `BTreeMap`, so this loop visits modules in name order and the
547    // warning always lands on the first `use` of the alphabetically first
548    // module that names it, which is what makes the outcome deterministic
549    // enough to test.
550    let mut warned_hosts: BTreeSet<String> = BTreeSet::new();
551    for (name, module) in &package.modules {
552        let uses = resolve_uses(
553            name,
554            module,
555            &surfaces,
556            schemas,
557            &mut errors,
558            &mut warnings,
559            &mut warned_hosts,
560        );
561        edges.extend(uses.edges.iter().cloned());
562        let (resolved, calls) = resolve_module(
563            name,
564            module,
565            uses,
566            &surfaces,
567            &opaque_fields,
568            schemas,
569            &mut errors,
570            &mut warnings,
571        );
572        for (key, shapes) in calls {
573            call_sites.insert((name.clone(), key), shapes);
574        }
575        program.modules.insert(name.clone(), resolved);
576    }
577
578    check_import_cycles(&edges, &mut errors);
579    check_method_collisions(&program, &mut errors);
580    let (call_graph, unresolved) = package_call_graph(&program, &call_sites);
581    merge_open_calls(&mut program, &unresolved);
582    propagate_capabilities(&mut program, &call_graph);
583    program.call_graph = call_graph;
584    check_bodies(&program, schemas, &mut errors, &mut warnings);
585
586    if errors.is_empty() {
587        program.notices = warnings;
588        Ok(program)
589    } else {
590        errors.extend(warnings);
591        Err(errors)
592    }
593}
594
595#[allow(clippy::too_many_arguments)]
596fn resolve_module(
597    name: &str,
598    module: &crate::package::Module,
599    uses: ModuleUses,
600    surfaces: &BTreeMap<&str, Surface>,
601    opaque_fields: &OpaqueFields,
602    schemas: &HostSchemas,
603    errors: &mut Vec<Diagnostic>,
604    warnings: &mut Vec<Diagnostic>,
605) -> (ResolvedModule, BTreeMap<FnKey, Vec<CallShape>>) {
606    let mut resolved = ResolvedModule {
607        name: name.to_string(),
608        host_uses: uses.host_uses.clone(),
609        host_items: uses.host_items.clone(),
610        imports: uses.imports.clone(),
611        module_imports: uses.module_imports.clone(),
612        ..ResolvedModule::default()
613    };
614
615    // Pass 2: top-level declarations, merged across every unit of the module.
616    let mut fn_spans: BTreeMap<String, Span> = BTreeMap::new();
617    let mut struct_spans: BTreeMap<String, Span> = BTreeMap::new();
618    let mut enum_spans: BTreeMap<String, Span> = BTreeMap::new();
619    let mut alias_spans: BTreeMap<String, Span> = BTreeMap::new();
620    let mut trait_spans: BTreeMap<String, Span> = BTreeMap::new();
621    let mut pending_impls: Vec<(&cove_syntax::ast::ImplBlock, Span)> = Vec::new();
622    // Raw call sites found in each declaration's body, resolved to call-graph
623    // edges once every declaration in the module is known (pass 4).
624    let mut call_sites: BTreeMap<FnKey, Vec<CallShape>> = BTreeMap::new();
625
626    for unit in &module.units {
627        for item in &unit.ast.items {
628            match &item.kind {
629                ItemKind::Fn(decl) => {
630                    if let Some(existing) =
631                        duplicate(&mut fn_spans, &decl.name.node, decl.name.span)
632                    {
633                        errors.push(duplicate_declaration(
634                            name,
635                            &decl.name.node,
636                            decl.name.span,
637                            existing,
638                        ));
639                        continue;
640                    }
641                    missing_doc(warnings, item, &decl.name.node, decl.name.span);
642                    let (capabilities, calls, open) = analyze_body(
643                        decl,
644                        &resolved.host_uses,
645                        &resolved.host_items,
646                        opaque_fields,
647                        schemas,
648                    );
649                    call_sites.insert(FnKey::Fn(decl.name.node.clone()), calls);
650                    resolved.functions.insert(
651                        decl.name.node.clone(),
652                        FnEntry {
653                            decl: Arc::new(decl.clone()),
654                            exported: item.exported,
655                            is_test: item.is_test,
656                            doc: item.doc.clone(),
657                            receiver_type: None,
658                            from_trait_default: None,
659                            direct_capabilities: capabilities,
660                            required_capabilities: BTreeSet::new(),
661                            direct_open_calls: open,
662                            open_calls: BTreeSet::new(),
663                        },
664                    );
665                }
666                ItemKind::Struct(decl) => {
667                    if let Some(existing) =
668                        duplicate(&mut struct_spans, &decl.name.node, decl.name.span)
669                    {
670                        errors.push(duplicate_declaration(
671                            name,
672                            &decl.name.node,
673                            decl.name.span,
674                            existing,
675                        ));
676                        continue;
677                    }
678                    missing_doc(warnings, item, &decl.name.node, decl.name.span);
679                    resolved.structs.insert(
680                        decl.name.node.clone(),
681                        StructEntry {
682                            decl: Arc::new(decl.clone()),
683                            exported: item.exported,
684                            opaque: item.is_opaque,
685                            doc: item.doc.clone(),
686                        },
687                    );
688                }
689                ItemKind::Enum(decl) => {
690                    if let Some(existing) =
691                        duplicate(&mut enum_spans, &decl.name.node, decl.name.span)
692                    {
693                        errors.push(duplicate_declaration(
694                            name,
695                            &decl.name.node,
696                            decl.name.span,
697                            existing,
698                        ));
699                        continue;
700                    }
701                    missing_doc(warnings, item, &decl.name.node, decl.name.span);
702                    resolved.enums.insert(
703                        decl.name.node.clone(),
704                        EnumEntry {
705                            decl: Arc::new(decl.clone()),
706                            exported: item.exported,
707                            doc: item.doc.clone(),
708                        },
709                    );
710                }
711                ItemKind::Trait(decl) => {
712                    if let Some(existing) =
713                        duplicate(&mut trait_spans, &decl.name.node, decl.name.span)
714                    {
715                        errors.push(duplicate_declaration(
716                            name,
717                            &decl.name.node,
718                            decl.name.span,
719                            existing,
720                        ));
721                        continue;
722                    }
723                    missing_doc(warnings, item, &decl.name.node, decl.name.span);
724                    // A trait's methods are part of the interface the trait
725                    // publishes, so an exported trait documents each of them.
726                    if item.exported {
727                        for method in &decl.methods {
728                            if method.doc.is_none() {
729                                warnings.push(undocumented(
730                                    &format!("{}.{}", decl.name.node, method.name.node),
731                                    method.name.span,
732                                ));
733                            }
734                        }
735                    }
736                    resolved.traits.insert(
737                        decl.name.node.clone(),
738                        TraitEntry {
739                            decl: Arc::new(decl.clone()),
740                            exported: item.exported,
741                            doc: item.doc.clone(),
742                        },
743                    );
744                }
745                ItemKind::TypeAlias(decl) => {
746                    if let Some(existing) =
747                        duplicate(&mut alias_spans, &decl.name.node, decl.name.span)
748                    {
749                        errors.push(duplicate_declaration(
750                            name,
751                            &decl.name.node,
752                            decl.name.span,
753                            existing,
754                        ));
755                        continue;
756                    }
757                    missing_doc(warnings, item, &decl.name.node, decl.name.span);
758                    resolved.aliases.insert(
759                        decl.name.node.clone(),
760                        AliasEntry {
761                            decl: Arc::new(decl.clone()),
762                            exported: item.exported,
763                            doc: item.doc.clone(),
764                        },
765                    );
766                }
767                ItemKind::Impl(impl_block) => {
768                    pending_impls.push((impl_block, item.span));
769                }
770            }
771        }
772    }
773
774    // Pass 3: `impl` blocks, once every struct, enum, and trait in the module
775    // is known.
776    let mut method_spans: BTreeMap<(String, String), Span> = BTreeMap::new();
777    for (impl_block, _impl_span) in pending_impls {
778        let type_name = impl_block.type_name.node.clone();
779        let declares_type =
780            resolved.structs.contains_key(&type_name) || resolved.enums.contains_key(&type_name);
781        // A conformance may name an imported type, so the type an `impl`
782        // extends is looked up the way every other name is: this module
783        // first, then what it imported.
784        let type_module = declaring_module_of(surfaces, name, &uses, &type_name, DeclKind::Type);
785
786        // The orphan rule: a conformance may only be declared where one of
787        // its two parties *is declared*. Imports widen which names an `impl`
788        // can spell, but not this: a third module that imports both a trait
789        // and a type still may not make one conform to the other, which is
790        // the whole point of the rule.
791        if let Some(trait_ident) = &impl_block.trait_name {
792            let trait_name = trait_ident.node.clone();
793            let declares_trait = resolved.traits.contains_key(&trait_name);
794            let trait_module =
795                declaring_module_of(surfaces, name, &uses, &trait_name, DeclKind::Trait);
796            if !declares_trait && !declares_type {
797                errors.push(orphan_conformance(
798                    name,
799                    &trait_name,
800                    &type_name,
801                    trait_ident.span.to(impl_block.type_name.span),
802                ));
803                continue;
804            }
805            // `Snapshot` is a builtin trait (see `builtin_snapshot_trait`): it
806            // belongs to no module, so it never has a `trait_module`, and the
807            // check below would otherwise reject it as unknown.
808            if trait_module.is_none() && trait_name != BUILTIN_SNAPSHOT_TRAIT {
809                errors.push(
810                    Diagnostic::error(
811                        "cove::resolve::unknown_trait",
812                        format!("`{trait_name}` names a trait module `{name}` can see"),
813                    )
814                    .at(trait_ident.span)
815                    .rule("A conformance names a trait the module declares or imports.")
816                    .help(format!(
817                        "Declare `trait {trait_name}` in this module, `use <module>.{trait_name}` to import it, or fix the name."
818                    )),
819                );
820                continue;
821            }
822        }
823
824        if type_module.is_none() {
825            errors.push(
826                Diagnostic::error(
827                    "cove::resolve::unknown_impl_type",
828                    format!("`impl {type_name}` names a type module `{name}` can see"),
829                )
830                .at(impl_block.type_name.span)
831                .rule("An `impl` block extends a struct or enum the module declares, or one it imports as part of a conformance.")
832                .help(format!(
833                    "Declare `struct {type_name}` or `enum {type_name}` in this module, `use <module>.{type_name}` to import it, or fix the name."
834                )),
835            );
836            continue;
837        }
838        let type_module = type_module.expect("checked just above").to_string();
839
840        // An inherent `impl` extends only a type this module declares: it is
841        // not a conformance, so the orphan rule has nothing to say about it,
842        // and adding methods to another module's type from outside would be
843        // exactly what that rule forbids.
844        if impl_block.trait_name.is_none() && !declares_type {
845            errors.push(
846                Diagnostic::error(
847                    "cove::resolve::foreign_inherent_impl",
848                    format!(
849                        "`impl {type_name}` adds methods to a type module `{type_module}` declares"
850                    ),
851                )
852                .at(impl_block.type_name.span)
853                .rule("An `impl` block with no trait extends a type its own module declares; a method for another module's type belongs to a trait, so that the conformance is a fact both modules can see.")
854                .help(format!(
855                    "move this block to module `{type_module}`, or declare a trait here and write `impl <Trait> for {type_name}`"
856                )),
857            );
858            continue;
859        }
860
861        if let Some(trait_ident) = &impl_block.trait_name {
862            let header = trait_ident.span.to(impl_block.type_name.span);
863            let key = (trait_ident.node.clone(), type_name.clone());
864            if let Some(existing) = resolved.conformances.get(&key) {
865                errors.push(
866                    Diagnostic::error(
867                        "cove::resolve::duplicate_conformance",
868                        format!("`{type_name}` already conforms to `{}`", trait_ident.node),
869                    )
870                    .at(header)
871                    .label(existing.span, "the first conformance is declared here")
872                    .rule("A type conforms to a trait exactly once; conformance is explicit, so two `impl Trait for Type` blocks would leave no way to choose.")
873                    .help("Merge the two blocks into one."),
874                );
875                continue;
876            }
877            let (trait_module, trait_decl) = match declaring_module_of(
878                surfaces,
879                name,
880                &uses,
881                &trait_ident.node,
882                DeclKind::Trait,
883            ) {
884                Some(module) => (
885                    module.to_string(),
886                    surfaces[module].traits[&trait_ident.node].clone(),
887                ),
888                // Only `Snapshot` reaches here: every other trait name was
889                // already rejected above. It belongs to no module, so it
890                // conforms wherever the type itself does.
891                None => (type_module.clone(), builtin_snapshot_trait(header)),
892            };
893            check_conformance(
894                &mut resolved,
895                name,
896                impl_block,
897                Conformance {
898                    trait_name: trait_ident.node.clone(),
899                    type_name: type_name.clone(),
900                    trait_module,
901                    type_module,
902                    methods: BTreeSet::new(),
903                    span: header,
904                },
905                trait_decl,
906                &mut method_spans,
907                &mut call_sites,
908                opaque_fields,
909                schemas,
910                errors,
911            );
912            continue;
913        }
914
915        for inner in &impl_block.items {
916            match &inner.kind {
917                ItemKind::Fn(decl) => {
918                    let key = (type_name.clone(), decl.name.node.clone());
919                    if let Some(existing_span) = method_spans.get(&key) {
920                        errors.push(
921                            Diagnostic::error(
922                                "cove::resolve::duplicate_declaration",
923                                format!(
924                                    "`{type_name}.{}` is declared twice in module `{name}`",
925                                    decl.name.node
926                                ),
927                            )
928                            .at(decl.name.span)
929                            .label(
930                                *existing_span,
931                                format!("`{}` first declared here", decl.name.node),
932                            )
933                            .rule(
934                                "Each method name may be declared once per type across a module's implementation units.",
935                            ),
936                        );
937                        continue;
938                    }
939                    method_spans.insert(key.clone(), decl.name.span);
940                    missing_doc(warnings, inner, &decl.name.node, decl.name.span);
941                    let (capabilities, calls, open) = analyze_body(
942                        decl,
943                        &resolved.host_uses,
944                        &resolved.host_items,
945                        opaque_fields,
946                        schemas,
947                    );
948                    call_sites.insert(
949                        FnKey::Method(type_name.clone(), decl.name.node.clone()),
950                        calls,
951                    );
952                    resolved.methods.insert(
953                        key,
954                        FnEntry {
955                            decl: Arc::new(decl.clone()),
956                            exported: inner.exported,
957                            // A method is reached through its type, never
958                            // through the test runner; the parser rejects a
959                            // `test fn` written in an `impl` block.
960                            is_test: false,
961                            doc: inner.doc.clone(),
962                            receiver_type: Some(type_name.clone()),
963                            from_trait_default: None,
964                            direct_capabilities: capabilities,
965                            required_capabilities: BTreeSet::new(),
966                            direct_open_calls: open,
967                            open_calls: BTreeSet::new(),
968                        },
969                    );
970                }
971                _ => {
972                    errors.push(
973                        Diagnostic::error(
974                            "cove::resolve::invalid_impl_item",
975                            "only `fn` declarations are allowed inside an `impl` block",
976                        )
977                        .at(inner.span)
978                        .rule("An `impl` block may only contain method declarations."),
979                    );
980                }
981            }
982        }
983    }
984
985    // The call sites found in passes 2 and 3 are resolved to call-graph
986    // edges by [`package_call_graph`], once every module is resolved: a call
987    // may reach an imported declaration, so the graph is the package's.
988    (resolved, call_sites)
989}
990
991// ------------------------------------------------------------------ imports
992
993/// The host modules a package may name without declaring them.
994///
995/// This is [`HostSchemas`], the one description of the host modules this
996/// compilation can see, rather than a list kept here in step with it: the
997/// compiler cannot ask the runtime, which depends on it, but it can read the
998/// schema both of them do. It used to be a hand-written array with a
999/// cross-crate test to catch the drift, written after `http` had already
1000/// drifted out of it and let a package module shadow the host module in
1001/// silence.
1002///
1003/// It is only consulted to refuse a package module that would shadow a host
1004/// module. A `use` naming a module that is not here is still accepted, since
1005/// a host may register any module it likes. Such a `use` warns instead: a
1006/// module no schema describes is checked by nothing until the run reaches the
1007/// boundary.
1008pub fn host_modules(schemas: &HostSchemas) -> impl Iterator<Item = &'static str> + '_ {
1009    schemas.names()
1010}
1011
1012/// What one module offers a `use` in another: every top-level declaration it
1013/// makes, what kind it is, whether it is exported, and where it was written.
1014///
1015/// This is collected before any module is resolved, because a `use` in one
1016/// module is answered by another module's declarations, and because an
1017/// `impl Trait for Type` may name an imported trait whose declaration has to
1018/// be read while the importing module is still being resolved.
1019///
1020/// Methods do not appear: a method is reached through its type, so importing
1021/// the type is what makes it visible.
1022#[derive(Debug, Default)]
1023struct Surface {
1024    declarations: BTreeMap<String, Declared>,
1025    /// The traits this module declares, whose method lists an
1026    /// `impl Trait for Type` in another module has to read.
1027    traits: BTreeMap<String, Arc<TraitDecl>>,
1028}
1029
1030#[derive(Debug)]
1031struct Declared {
1032    kind: DeclKind,
1033    exported: bool,
1034    span: Span,
1035}
1036
1037/// What a declaration is, to the extent a `use` and an `impl` need to know.
1038#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1039enum DeclKind {
1040    Function,
1041    /// A struct or an enum: the two kinds an `impl` block may extend.
1042    Type,
1043    Trait,
1044    Alias,
1045}
1046
1047impl Surface {
1048    fn of(module: &crate::package::Module) -> Surface {
1049        let mut declarations: BTreeMap<String, Declared> = BTreeMap::new();
1050        let mut traits: BTreeMap<String, Arc<TraitDecl>> = BTreeMap::new();
1051        for unit in &module.units {
1052            for item in &unit.ast.items {
1053                if let ItemKind::Trait(decl) = &item.kind {
1054                    traits
1055                        .entry(decl.name.node.clone())
1056                        .or_insert_with(|| Arc::new(decl.clone()));
1057                }
1058                let (name, kind) = match &item.kind {
1059                    ItemKind::Fn(decl) => (&decl.name, DeclKind::Function),
1060                    ItemKind::Struct(decl) => (&decl.name, DeclKind::Type),
1061                    ItemKind::Enum(decl) => (&decl.name, DeclKind::Type),
1062                    ItemKind::Trait(decl) => (&decl.name, DeclKind::Trait),
1063                    ItemKind::TypeAlias(decl) => (&decl.name, DeclKind::Alias),
1064                    ItemKind::Impl(_) => continue,
1065                };
1066                declarations.entry(name.node.clone()).or_insert(Declared {
1067                    kind,
1068                    exported: item.exported,
1069                    span: name.span,
1070                });
1071            }
1072        }
1073        Surface {
1074            declarations,
1075            traits,
1076        }
1077    }
1078
1079    /// Whether this module declares `name` as `kind`.
1080    fn declares(&self, name: &str, kind: DeclKind) -> bool {
1081        self.declarations
1082            .get(name)
1083            .is_some_and(|declared| declared.kind == kind)
1084    }
1085}
1086
1087/// The declaration a name in module `module` refers to, when some module of
1088/// the package declares it as `kind`: the module that declares it, and the
1089/// name itself.
1090///
1091/// A module's own declaration answers first, and an import answers second —
1092/// the same order every other lookup uses. A `use` cannot bind a name the
1093/// module declares, so at most one of the two ever applies.
1094fn declaring_module_of<'a>(
1095    surfaces: &'a BTreeMap<&'a str, Surface>,
1096    module: &'a str,
1097    uses: &'a ModuleUses,
1098    name: &str,
1099    kind: DeclKind,
1100) -> Option<&'a str> {
1101    if surfaces
1102        .get(module)
1103        .is_some_and(|surface| surface.declares(name, kind))
1104    {
1105        return Some(module);
1106    }
1107    let owner = uses.imports.get(name)?.as_str();
1108    surfaces
1109        .get(owner)
1110        .is_some_and(|surface| surface.declares(name, kind))
1111        .then_some(owner)
1112}
1113
1114/// One module's resolved `use` declarations.
1115#[derive(Debug, Default)]
1116struct ModuleUses {
1117    imports: BTreeMap<String, String>,
1118    module_imports: BTreeMap<String, String>,
1119    host_uses: BTreeSet<String>,
1120    host_items: BTreeMap<String, String>,
1121    /// One edge per `use` that names a module of this package, for the
1122    /// cycle check.
1123    edges: Vec<ImportEdge>,
1124}
1125
1126/// A module dependency, with the `use` that created it so a cycle can point
1127/// at the line that closes it.
1128#[derive(Clone, Debug)]
1129struct ImportEdge {
1130    from: String,
1131    to: String,
1132    span: Span,
1133}
1134
1135/// What one `use` binds in the module that writes it, for conflict reports.
1136#[derive(Clone, Debug)]
1137enum Bound {
1138    /// `use console.println` binds `println` to a host module.
1139    HostItem(String),
1140    /// `use booking.create` binds `create` to a module's declaration.
1141    Item(String),
1142    /// `use booking` binds `booking` to a whole module.
1143    Module(String),
1144}
1145
1146impl Bound {
1147    fn describe(&self) -> String {
1148        match self {
1149            Bound::HostItem(host) => format!("the host module `{host}`"),
1150            Bound::Item(module) => format!("module `{module}`"),
1151            Bound::Module(module) => format!("the module `{module}`"),
1152        }
1153    }
1154}
1155
1156/// Resolves every `use` of one module.
1157///
1158/// ADR 0005: a dotted path resolves against the package's modules first and
1159/// the host registry second, so a package's own structure does not change
1160/// meaning because a host gained an operation. Concretely, for a path `p`:
1161///
1162/// 1. when `p` names a module, it imports that module, whose exports are
1163///    then reachable qualified;
1164/// 2. otherwise, when `p` without its last segment names a module, it
1165///    imports that module's declaration of the last segment, which must be
1166///    exported;
1167/// 3. otherwise it is a host path: one segment names a host module, two name
1168///    a host module and one operation, and anything longer matches nothing
1169///    and is reported.
1170///
1171/// A module that shares a name with a host module is refused rather than
1172/// preferred, since the host namespace is not the package's to change.
1173fn resolve_uses(
1174    name: &str,
1175    module: &crate::package::Module,
1176    surfaces: &BTreeMap<&str, Surface>,
1177    schemas: &HostSchemas,
1178    errors: &mut Vec<Diagnostic>,
1179    warnings: &mut Vec<Diagnostic>,
1180    warned_hosts: &mut BTreeSet<String>,
1181) -> ModuleUses {
1182    let mut uses = ModuleUses::default();
1183    let mut bound: BTreeMap<String, (Bound, Span)> = BTreeMap::new();
1184    let own = surfaces.get(name);
1185
1186    for unit in &module.units {
1187        for use_decl in &unit.ast.uses {
1188            let segments: Vec<&str> = use_decl.path.iter().map(|i| i.node.as_str()).collect();
1189            let path = segments.join(".");
1190            let span = use_decl.span;
1191            let last = segments.last().expect("a `use` path is never empty");
1192
1193            if surfaces.contains_key(path.as_str()) {
1194                if let Some(diagnostic) = shadowed_host(&path, schemas, span) {
1195                    errors.push(diagnostic);
1196                    continue;
1197                }
1198                if let Some(diagnostic) = ambiguous_module_path(&path, &segments, surfaces, span) {
1199                    errors.push(diagnostic);
1200                    continue;
1201                }
1202                bind(
1203                    &mut bound,
1204                    name,
1205                    own,
1206                    last,
1207                    Bound::Module(path.clone()),
1208                    span,
1209                    errors,
1210                );
1211                uses.module_imports.insert(last.to_string(), path.clone());
1212                uses.edges.push(ImportEdge {
1213                    from: name.to_string(),
1214                    to: path,
1215                    span,
1216                });
1217                continue;
1218            }
1219
1220            if segments.len() >= 2 {
1221                let owner = segments[..segments.len() - 1].join(".");
1222                if let Some(surface) = surfaces.get(owner.as_str()) {
1223                    if let Some(diagnostic) = shadowed_host(&owner, schemas, span) {
1224                        errors.push(diagnostic);
1225                        continue;
1226                    }
1227                    match surface.declarations.get(*last) {
1228                        Some(declared) if declared.exported => {
1229                            bind(
1230                                &mut bound,
1231                                name,
1232                                own,
1233                                last,
1234                                Bound::Item(owner.clone()),
1235                                span,
1236                                errors,
1237                            );
1238                            uses.imports.insert(last.to_string(), owner.clone());
1239                            uses.edges.push(ImportEdge {
1240                                from: name.to_string(),
1241                                to: owner,
1242                                span,
1243                            });
1244                        }
1245                        Some(declared) => {
1246                            errors.push(private_declaration(&owner, last, span, declared.span))
1247                        }
1248                        None => errors.push(no_such_declaration(&owner, last, surface, span)),
1249                    }
1250                    continue;
1251                }
1252            }
1253
1254            match segments.len() {
1255                1 => {
1256                    warn_unchecked_host_once(&path, schemas, span, warned_hosts, warnings);
1257                    uses.host_uses.insert(path);
1258                }
1259                2 => {
1260                    let host = segments[0].to_string();
1261                    warn_unchecked_host_once(&host, schemas, span, warned_hosts, warnings);
1262                    uses.host_uses.insert(host.clone());
1263                    bind(
1264                        &mut bound,
1265                        name,
1266                        own,
1267                        last,
1268                        Bound::HostItem(host.clone()),
1269                        span,
1270                        errors,
1271                    );
1272                    uses.host_items.insert(last.to_string(), host);
1273                }
1274                _ => errors.push(unknown_use(&path, &segments, surfaces, span)),
1275            }
1276        }
1277    }
1278
1279    uses
1280}
1281
1282/// Records that `use` binds `name` in the importing module, reporting a name
1283/// it already binds and a name the importing module declares itself.
1284///
1285/// A repeated `use` of the same thing is not a conflict: writing
1286/// `use booking.create` in two units of one module means the same import
1287/// twice.
1288fn bind(
1289    bound: &mut BTreeMap<String, (Bound, Span)>,
1290    module: &str,
1291    own: Option<&Surface>,
1292    name: &str,
1293    what: Bound,
1294    span: Span,
1295    errors: &mut Vec<Diagnostic>,
1296) {
1297    if let Some(declared) = own.and_then(|surface| surface.declarations.get(name)) {
1298        errors.push(
1299            Diagnostic::error(
1300                "cove::resolve::import_conflict",
1301                format!("`{name}` is imported, but module `{module}` also declares it"),
1302            )
1303            .at(span)
1304            .label(declared.span, format!("`{name}` is declared here"))
1305            .rule("An imported name and a declared name cannot both mean `name` in one module.")
1306            .help("rename one of them, or drop the `use` and name the import qualified"),
1307        );
1308        return;
1309    }
1310    match bound.get(name) {
1311        Some((existing, existing_span)) if !same_origin(existing, &what) => {
1312            // Two host modules disagreeing about one unqualified name is the
1313            // case that predates module imports, and keeps its own code.
1314            let code = match (existing, &what) {
1315                (Bound::HostItem(_), Bound::HostItem(_)) => "cove::resolve::ambiguous_use",
1316                _ => "cove::resolve::import_conflict",
1317            };
1318            errors.push(
1319                Diagnostic::error(
1320                    code,
1321                    format!(
1322                        "`{name}` is imported from both {} and {}",
1323                        existing.describe(),
1324                        what.describe()
1325                    ),
1326                )
1327                .at(span)
1328                .label(
1329                    *existing_span,
1330                    format!("first imported from {} here", existing.describe()),
1331                )
1332                .rule("A `use` name must resolve to exactly one declaration or host module.")
1333                .help(format!(
1334                    "drop one of the two `use` declarations, and name `{name}` qualified where the other meaning is wanted"
1335                )),
1336            );
1337        }
1338        Some(_) => {}
1339        None => {
1340            bound.insert(name.to_string(), (what, span));
1341        }
1342    }
1343}
1344
1345fn same_origin(a: &Bound, b: &Bound) -> bool {
1346    match (a, b) {
1347        (Bound::HostItem(a), Bound::HostItem(b))
1348        | (Bound::Item(a), Bound::Item(b))
1349        | (Bound::Module(a), Bound::Module(b)) => a == b,
1350        _ => false,
1351    }
1352}
1353
1354/// Warns about a `use` of a host module no schema describes.
1355///
1356/// Such a module is the one case the checker genuinely cannot answer for: a
1357/// host may register anything, and until this compilation is handed its
1358/// table, every call into it is unknown here and checked for the first time
1359/// at the boundary. That fallback is deliberate and stays, but a program
1360/// resting on it should say so rather than read like one that checked.
1361fn unchecked_host_module(module: &str, schemas: &HostSchemas, span: Span) -> Option<Diagnostic> {
1362    if schemas.module(module).is_some() {
1363        return None;
1364    }
1365    Some(
1366        Diagnostic::warning(
1367            "cove::resolve::unchecked_host",
1368            format!("no Host API schema describes the host module `{module}`, so calls into it are unchecked"),
1369        )
1370        .at(span)
1371        .rule(
1372            "A Host API call is checked against its module's schema; the checker reads the shipped schemas and any an embedder supplies.",
1373        )
1374        .help(format!(
1375            "if `{module}` is an embedder's module, hand its `ModuleSchema` to the compiler with `Compiler::new().with_host_schema(...)`; otherwise check the spelling"
1376        )),
1377    )
1378}
1379
1380/// Warns about `module` once per package, on the first `use` that named it.
1381///
1382/// A `use` of an undescribed host module says the same thing every time it
1383/// is written, whether that is twice in one module (`use company` and
1384/// `use company.employee`) or once in each of several modules, so repeating
1385/// the warning would only inflate a `cove check --deny-warnings` count
1386/// without telling the reader anything new. `warned_hosts` is the memory
1387/// that makes it fire once: shared across every module of the package by the
1388/// caller, and updated here exactly when a warning is actually produced.
1389fn warn_unchecked_host_once(
1390    module: &str,
1391    schemas: &HostSchemas,
1392    span: Span,
1393    warned_hosts: &mut BTreeSet<String>,
1394    warnings: &mut Vec<Diagnostic>,
1395) {
1396    if warned_hosts.contains(module) {
1397        return;
1398    }
1399    if let Some(warning) = unchecked_host_module(module, schemas, span) {
1400        warned_hosts.insert(module.to_string());
1401        warnings.push(warning);
1402    }
1403}
1404
1405/// Refuses a module that shares its name with a host module.
1406///
1407/// Modules resolve first, so such a module would silently make the host
1408/// module unreachable for the whole package.
1409fn shadowed_host(module: &str, schemas: &HostSchemas, span: Span) -> Option<Diagnostic> {
1410    if !host_modules(schemas).any(|host| host == module) {
1411        return None;
1412    }
1413    Some(
1414        Diagnostic::error(
1415            "cove::resolve::module_shadows_host",
1416            format!("module `{module}` has the same name as the host module `{module}`"),
1417        )
1418        .at(span)
1419        .rule(
1420            "`use` resolves against the package's modules first, so a module named after a host module hides it.",
1421        )
1422        .help(format!(
1423            "rename the `{module}` module; the host namespace is not this package's to change"
1424        )),
1425    )
1426}
1427
1428/// Refuses a path that names a module *and* an exported declaration of the
1429/// module one segment shorter, which ADR 0005 does not settle.
1430fn ambiguous_module_path(
1431    path: &str,
1432    segments: &[&str],
1433    surfaces: &BTreeMap<&str, Surface>,
1434    span: Span,
1435) -> Option<Diagnostic> {
1436    if segments.len() < 2 {
1437        return None;
1438    }
1439    let owner = segments[..segments.len() - 1].join(".");
1440    let last = segments[segments.len() - 1];
1441    let declared = surfaces
1442        .get(owner.as_str())?
1443        .declarations
1444        .get(last)
1445        .filter(|declared| declared.exported)?;
1446    Some(
1447        Diagnostic::error(
1448            "cove::resolve::ambiguous_use",
1449            format!("`use {path}` names both the module `{path}` and `{last}`, exported by module `{owner}`"),
1450        )
1451        .at(span)
1452        .label(declared.span, format!("`{last}` is declared here"))
1453        .rule("A `use` path must have exactly one meaning.")
1454        .help(format!(
1455            "rename the `{path}` module or `{owner}.{last}`, so the path names one of them"
1456        )),
1457    )
1458}
1459
1460fn private_declaration(module: &str, name: &str, span: Span, declared: Span) -> Diagnostic {
1461    Diagnostic::error(
1462        "cove::resolve::private_declaration",
1463        format!("`{name}` is declared by module `{module}`, but is not exported"),
1464    )
1465    .at(span)
1466    .label(
1467        declared,
1468        format!("`{name}` is declared here, without `export`"),
1469    )
1470    .rule("An `export` declaration is public; other declarations are module-private.")
1471    .help(format!(
1472        "write `export` on `{name}` in module `{module}`, or import something else"
1473    ))
1474}
1475
1476fn no_such_declaration(module: &str, name: &str, surface: &Surface, span: Span) -> Diagnostic {
1477    let exported: Vec<String> = surface
1478        .declarations
1479        .iter()
1480        .filter(|(_, declared)| declared.exported)
1481        .map(|(name, _)| name.clone())
1482        .collect();
1483    Diagnostic::error(
1484        "cove::resolve::unknown_use",
1485        format!("module `{module}` declares no `{name}`, and `{module}` is not a host module"),
1486    )
1487    .at(span)
1488    .rule("`use` names a module of this package, one of its exported declarations, a host module, or one host operation.")
1489    .help(if exported.is_empty() {
1490        format!("module `{module}` exports nothing; write `export` on the declaration to import")
1491    } else {
1492        format!("module `{module}` exports {}", list_backticked(&exported))
1493    })
1494}
1495
1496fn unknown_use(
1497    path: &str,
1498    segments: &[&str],
1499    surfaces: &BTreeMap<&str, Surface>,
1500    span: Span,
1501) -> Diagnostic {
1502    let owner = segments[..segments.len() - 1].join(".");
1503    let modules: Vec<String> = surfaces.keys().map(|name| name.to_string()).collect();
1504    Diagnostic::error(
1505        "cove::resolve::unknown_use",
1506        format!("`use {path}` names neither a module of this package nor a host module"),
1507    )
1508    .at(span)
1509    .rule("`use` resolves against the package's modules first and the host registry second.")
1510    .help(format!(
1511        "there is no module `{path}` or `{owner}`; this package declares {}, and a host path names a module (`use console`) or one operation (`use console.println`)",
1512        list_backticked(&modules)
1513    ))
1514}
1515
1516/// Rejects one type having two methods of one name, when they were declared
1517/// in different modules.
1518///
1519/// A conformance may be declared where the trait is, for a type declared
1520/// elsewhere, so a type's methods no longer all come from one module — and
1521/// the per-module duplicate check cannot see the other one. Two candidates
1522/// with no rule to choose between them is the same mistake wherever the two
1523/// are written: `impl Trait for Type` in the trait's module and an inherent
1524/// method of that name in the type's own would leave the checker and the
1525/// interpreter free to pick differently.
1526fn check_method_collisions(program: &Program, errors: &mut Vec<Diagnostic>) {
1527    /// One method of one type: the module that declares the type, the type,
1528    /// and the method name.
1529    type MethodOf<'a> = (&'a str, &'a str, &'a str);
1530    /// Where a method was written: the module, and the name's span.
1531    type Site<'a> = (&'a str, Span);
1532
1533    let mut declared: BTreeMap<MethodOf, Vec<Site>> = BTreeMap::new();
1534    for (module, resolved) in &program.modules {
1535        for ((type_name, method), entry) in &resolved.methods {
1536            let Some(owner) = resolved.owner_of(type_name) else {
1537                continue;
1538            };
1539            declared
1540                .entry((owner, type_name.as_str(), method.as_str()))
1541                .or_default()
1542                .push((module.as_str(), entry.decl.name.span));
1543        }
1544    }
1545
1546    for ((type_module, type_name, method), sites) in declared {
1547        let [(first_module, first), rest @ ..] = sites.as_slice() else {
1548            continue;
1549        };
1550        for (module, span) in rest {
1551            errors.push(
1552                Diagnostic::error(
1553                    "cove::resolve::duplicate_declaration",
1554                    format!(
1555                        "`{type_name}.{method}` is declared in module `{module}` and in module `{first_module}`"
1556                    ),
1557                )
1558                .at(*span)
1559                .label(*first, format!("`{method}` first declared here"))
1560                .rule(
1561                    "Each method name may be declared once per type, across every module: a conformance declared where its trait is must not collide with a method of the type's own module.",
1562                )
1563                .help(format!(
1564                    "rename one of them, or move both into module `{type_module}`, which declares `{type_name}`"
1565                )),
1566            );
1567        }
1568    }
1569}
1570
1571/// Rejects a module that imports, directly or transitively, a module that
1572/// imports it.
1573///
1574/// ADR 0001 left "how are dependency cycles represented and diagnosed" open;
1575/// ADR 0005 answers it by forbidding them, so a package whose modules form a
1576/// cycle has a structure its author can see and fix.
1577fn check_import_cycles(edges: &[ImportEdge], errors: &mut Vec<Diagnostic>) {
1578    let mut graph: BTreeMap<&str, Vec<&ImportEdge>> = BTreeMap::new();
1579    for edge in edges {
1580        graph.entry(edge.from.as_str()).or_default().push(edge);
1581    }
1582
1583    let mut settled: BTreeSet<&str> = BTreeSet::new();
1584    let mut reported: BTreeSet<Vec<&str>> = BTreeSet::new();
1585    let roots: Vec<&str> = graph.keys().copied().collect();
1586    for root in roots {
1587        let mut stack: Vec<&ImportEdge> = Vec::new();
1588        walk_imports(
1589            root,
1590            &graph,
1591            &mut Vec::new(),
1592            &mut stack,
1593            &mut settled,
1594            &mut reported,
1595            errors,
1596        );
1597    }
1598}
1599
1600/// Depth-first walk over the module dependency graph, reporting the first
1601/// time each cycle is closed.
1602fn walk_imports<'a>(
1603    module: &'a str,
1604    graph: &BTreeMap<&'a str, Vec<&'a ImportEdge>>,
1605    path: &mut Vec<&'a str>,
1606    stack: &mut Vec<&'a ImportEdge>,
1607    settled: &mut BTreeSet<&'a str>,
1608    reported: &mut BTreeSet<Vec<&'a str>>,
1609    errors: &mut Vec<Diagnostic>,
1610) {
1611    if settled.contains(module) {
1612        return;
1613    }
1614    if let Some(start) = path.iter().position(|name| *name == module) {
1615        let mut cycle: Vec<&str> = path[start..].to_vec();
1616        cycle.push(module);
1617        let closing = *stack.last().expect("a cycle is closed by an edge");
1618        // One cycle is reachable from every module on it; report it once,
1619        // keyed by its members rather than by where the walk entered it.
1620        let mut members: Vec<&str> = cycle.clone();
1621        members.sort();
1622        members.dedup();
1623        if reported.insert(members) {
1624            errors.push(
1625                Diagnostic::error(
1626                    "cove::resolve::import_cycle",
1627                    format!(
1628                        "module `{module}` imports itself through {}",
1629                        cycle.join(" -> ")
1630                    ),
1631                )
1632                .at(closing.span)
1633                .rule(
1634                    "A module may not import, directly or transitively, a module that imports it.",
1635                )
1636                .help("move what both modules need into a third module they can each import"),
1637            );
1638        }
1639        return;
1640    }
1641
1642    path.push(module);
1643    for edge in graph.get(module).into_iter().flatten() {
1644        stack.push(edge);
1645        walk_imports(&edge.to, graph, path, stack, settled, reported, errors);
1646        stack.pop();
1647    }
1648    path.pop();
1649    settled.insert(module);
1650}
1651
1652/// The name of Cove's builtin `Snapshot` trait.
1653///
1654/// Unlike every other trait, `Snapshot` is not written anywhere in Cove
1655/// source: it belongs to no module, so an `impl Snapshot for Type` conforms
1656/// wherever `Type` itself is declared, without a `trait Snapshot { ... }`
1657/// declaration or a `use` to reach one. This is a deliberate, narrow
1658/// departure from ADR 0006's "conformance is explicit... there is no
1659/// blanket implementation": the alternative was extending the trait grammar
1660/// with a `Self` return type solely so the compiler's own prelude could
1661/// spell one trait, which the MVP's "no associated types" already rules out
1662/// for user-written traits.
1663const BUILTIN_SNAPSHOT_TRAIT: &str = "Snapshot";
1664
1665/// Synthesizes `Snapshot`'s one method, `fn snapshot(self) -> Self`, at the
1666/// header span of the `impl Snapshot for Type` block that needs it.
1667///
1668/// `Self` is not a type the language can otherwise write — the MVP has no
1669/// associated types — so this is built directly rather than parsed. Nothing
1670/// downstream needs it to be: `check_conformance` only checks method names,
1671/// not their types, and each conformance declares its own concrete return
1672/// type exactly like any other trait method.
1673fn builtin_snapshot_trait(span: Span) -> Arc<TraitDecl> {
1674    Arc::new(TraitDecl {
1675        name: Spanned::new(BUILTIN_SNAPSHOT_TRAIT.to_string(), span),
1676        methods: vec![TraitMethod {
1677            doc: Some(
1678                "Returns an independent, mutable copy of this value's own graph, preserving \
1679                 cycles and internal sharing where it has any."
1680                    .to_string(),
1681            ),
1682            name: Spanned::new("snapshot".to_string(), span),
1683            is_async: false,
1684            receiver: Some(Receiver {
1685                is_var: false,
1686                span,
1687            }),
1688            params: Vec::new(),
1689            return_type: None,
1690            default: None,
1691            span,
1692        }],
1693        span,
1694    })
1695}
1696
1697/// Checks one `impl Trait for Type` block and records the conformance it
1698/// declares.
1699///
1700/// A conformance must supply every method the trait declares without a
1701/// default body, and may supply no method the trait does not declare. A
1702/// method the trait defaults and the block does not override is recorded as
1703/// the type's own method, running the trait's body, so that dispatch never
1704/// has to ask where a method came from.
1705#[allow(clippy::too_many_arguments)]
1706fn check_conformance(
1707    resolved: &mut ResolvedModule,
1708    module: &str,
1709    impl_block: &cove_syntax::ast::ImplBlock,
1710    conformance: Conformance,
1711    trait_decl: Arc<TraitDecl>,
1712    method_spans: &mut BTreeMap<(String, String), Span>,
1713    call_sites: &mut BTreeMap<FnKey, Vec<CallShape>>,
1714    opaque_fields: &OpaqueFields,
1715    schemas: &HostSchemas,
1716    errors: &mut Vec<Diagnostic>,
1717) {
1718    let Conformance {
1719        trait_name,
1720        type_name,
1721        span: header,
1722        ..
1723    } = conformance.clone();
1724    // A conformance's methods are as public as the pair it joins: a trait
1725    // declared elsewhere is one this module imported, which it could only do
1726    // if the trait is exported.
1727    let trait_exported = resolved
1728        .traits
1729        .get(&trait_name)
1730        .map(|entry| entry.exported)
1731        .unwrap_or(true);
1732    let mut supplied: BTreeSet<String> = BTreeSet::new();
1733
1734    for inner in &impl_block.items {
1735        let ItemKind::Fn(decl) = &inner.kind else {
1736            errors.push(
1737                Diagnostic::error(
1738                    "cove::resolve::invalid_impl_item",
1739                    "only `fn` declarations are allowed inside an `impl` block",
1740                )
1741                .at(inner.span)
1742                .rule("An `impl` block may only contain method declarations."),
1743            );
1744            continue;
1745        };
1746        let method_name = decl.name.node.clone();
1747        let Some(declared) = trait_decl
1748            .methods
1749            .iter()
1750            .find(|m| m.name.node == method_name)
1751        else {
1752            errors.push(
1753                Diagnostic::error(
1754                    "cove::resolve::unknown_trait_method",
1755                    format!("`{trait_name}` declares no method `{method_name}`"),
1756                )
1757                .at(decl.name.span)
1758                .label(trait_decl.name.span, format!("`{trait_name}` is declared here"))
1759                .rule("An `impl Trait for Type` block supplies exactly the methods the trait declares; anything else belongs in the type's own `impl` block.")
1760                .help(format!(
1761                    "declare `{method_name}` in `trait {trait_name}`, or move it to `impl {type_name}`"
1762                )),
1763            );
1764            continue;
1765        };
1766        supplied.insert(method_name);
1767        record_method(
1768            resolved,
1769            module,
1770            &type_name,
1771            Arc::new(decl.clone()),
1772            trait_exported,
1773            declared.doc.clone().or_else(|| inner.doc.clone()),
1774            None,
1775            method_spans,
1776            call_sites,
1777            opaque_fields,
1778            schemas,
1779            errors,
1780        );
1781    }
1782
1783    let missing: Vec<String> = trait_decl
1784        .methods
1785        .iter()
1786        .filter(|m| m.default.is_none() && !supplied.contains(&m.name.node))
1787        .map(|m| m.name.node.clone())
1788        .collect();
1789    if !missing.is_empty() {
1790        errors.push(
1791            Diagnostic::error(
1792                "cove::resolve::missing_trait_method",
1793                format!(
1794                    "`{type_name}` does not conform to `{trait_name}`: missing {}",
1795                    list_backticked(&missing)
1796                ),
1797            )
1798            .at(header)
1799            .label(
1800                trait_decl.name.span,
1801                format!("`{trait_name}` declares {}", list_backticked(&missing)),
1802            )
1803            .rule("A conformance supplies every method its trait declares without a default body.")
1804            .help(format!(
1805                "add {} to this block",
1806                missing
1807                    .iter()
1808                    .map(|m| format!("`fn {m}(...)`"))
1809                    .collect::<Vec<_>>()
1810                    .join(", ")
1811            )),
1812        );
1813    }
1814
1815    // A defaulted method the block did not override becomes the type's own
1816    // method, with the trait's body.
1817    let mut methods = supplied.clone();
1818    for method in &trait_decl.methods {
1819        if supplied.contains(&method.name.node) {
1820            continue;
1821        }
1822        let Some(body) = &method.default else {
1823            continue;
1824        };
1825        methods.insert(method.name.node.clone());
1826        let decl = Arc::new(FnDecl {
1827            name: method.name.clone(),
1828            is_async: method.is_async,
1829            generics: Vec::new(),
1830            receiver: method.receiver,
1831            params: method.params.clone(),
1832            return_type: method.return_type.clone(),
1833            body: body.clone(),
1834            span: method.span,
1835        });
1836        record_method(
1837            resolved,
1838            module,
1839            &type_name,
1840            decl,
1841            trait_exported,
1842            method.doc.clone(),
1843            Some(trait_name.clone()),
1844            method_spans,
1845            call_sites,
1846            opaque_fields,
1847            schemas,
1848            errors,
1849        );
1850    }
1851
1852    resolved.conformances.insert(
1853        (trait_name, type_name),
1854        Conformance {
1855            methods,
1856            ..conformance
1857        },
1858    );
1859}
1860
1861/// Records one method of `type_name`, rejecting a second declaration of the
1862/// same name whatever `impl` block it came from: a trait method and an
1863/// inherent method of the same name would leave a call site with two
1864/// candidates and no rule to choose between them.
1865#[allow(clippy::too_many_arguments)]
1866fn record_method(
1867    resolved: &mut ResolvedModule,
1868    module: &str,
1869    type_name: &str,
1870    decl: Arc<FnDecl>,
1871    exported: bool,
1872    doc: Option<String>,
1873    from_trait_default: Option<String>,
1874    method_spans: &mut BTreeMap<(String, String), Span>,
1875    call_sites: &mut BTreeMap<FnKey, Vec<CallShape>>,
1876    opaque_fields: &OpaqueFields,
1877    schemas: &HostSchemas,
1878    errors: &mut Vec<Diagnostic>,
1879) {
1880    let key = (type_name.to_string(), decl.name.node.clone());
1881    if let Some(existing_span) = method_spans.get(&key) {
1882        errors.push(
1883            Diagnostic::error(
1884                "cove::resolve::duplicate_declaration",
1885                format!(
1886                    "`{type_name}.{}` is declared twice in module `{module}`",
1887                    decl.name.node
1888                ),
1889            )
1890            .at(decl.name.span)
1891            .label(
1892                *existing_span,
1893                format!("`{}` first declared here", decl.name.node),
1894            )
1895            .rule(
1896                "Each method name may be declared once per type across a module's implementation units.",
1897            ),
1898        );
1899        return;
1900    }
1901    method_spans.insert(key.clone(), decl.name.span);
1902    let (capabilities, calls, open) = analyze_body(
1903        &decl,
1904        &resolved.host_uses,
1905        &resolved.host_items,
1906        opaque_fields,
1907        schemas,
1908    );
1909    call_sites.insert(
1910        FnKey::Method(type_name.to_string(), decl.name.node.clone()),
1911        calls,
1912    );
1913    resolved.methods.insert(
1914        key,
1915        FnEntry {
1916            decl,
1917            exported,
1918            // A method is never a test; see the `is_test` field.
1919            is_test: false,
1920            doc,
1921            receiver_type: Some(type_name.to_string()),
1922            from_trait_default,
1923            direct_capabilities: capabilities,
1924            required_capabilities: BTreeSet::new(),
1925            direct_open_calls: open,
1926            open_calls: BTreeSet::new(),
1927        },
1928    );
1929}
1930
1931/// The orphan rule from ADR 0006, stated where it is broken.
1932fn orphan_conformance(module: &str, trait_name: &str, type_name: &str, span: Span) -> Diagnostic {
1933    Diagnostic::error(
1934        "cove::resolve::orphan_conformance",
1935        format!(
1936            "module `{module}` declares neither `{trait_name}` nor `{type_name}`, so it cannot make one conform to the other"
1937        ),
1938    )
1939    .at(span)
1940    .rule("An `impl Trait for Type` is allowed only in the module that declares the trait or the module that declares the type, so that a conformance cannot appear from a module neither party knows about.")
1941    .help(format!(
1942        "move this block to the module that declares `{trait_name}` or the one that declares `{type_name}`"
1943    ))
1944}
1945
1946/// Records `name` at `span` in `spans`, returning the previous span if `name`
1947/// was already declared.
1948fn duplicate(spans: &mut BTreeMap<String, Span>, name: &str, span: Span) -> Option<Span> {
1949    if let Some(existing) = spans.get(name) {
1950        return Some(*existing);
1951    }
1952    spans.insert(name.to_string(), span);
1953    None
1954}
1955
1956fn duplicate_declaration(module: &str, name: &str, span: Span, first: Span) -> Diagnostic {
1957    Diagnostic::error(
1958        "cove::resolve::duplicate_declaration",
1959        format!("`{name}` is declared twice in module `{module}`"),
1960    )
1961    .at(span)
1962    .label(first, format!("`{name}` first declared here"))
1963    .rule("Each name may be declared once per module across its implementation units.")
1964}
1965
1966/// The Language Card warns on an exported declaration with no doc comment.
1967fn missing_doc(warnings: &mut Vec<Diagnostic>, item: &Item, name: &str, span: Span) {
1968    if item.exported && item.doc.is_none() {
1969        warnings.push(undocumented(name, span));
1970    }
1971}
1972
1973/// The `missing_doc` warning itself, for the declarations that are not
1974/// [`Item`]s of their own — a trait's methods.
1975fn undocumented(name: &str, span: Span) -> Diagnostic {
1976    Diagnostic::warning(
1977        "cove::resolve::missing_doc",
1978        format!("exported `{name}` has no doc comment"),
1979    )
1980    .at(span)
1981    .rule("Public declarations without doc comments warn by default.")
1982    .help(format!("Add a `///` doc comment above `{name}`."))
1983}
1984
1985/// Every field name in the package declared with a type whose implementation
1986/// its producer chose, split by whether reading the field *is* holding such a
1987/// value or merely holding a container of them.
1988///
1989/// A body reaches a `dyn Trait` through a field far more often than it names
1990/// one: `struct Box { item: dyn Summary }` is written once, and every method
1991/// of `Box` then dispatches through `self.item` without the type appearing
1992/// again. Seeding the walk from parameters alone missed that shape entirely,
1993/// and a missed shape is the lower-bound-presented-as-complete failure
1994/// ADR 0015 exists to rule out.
1995///
1996/// The set is keyed by field *name*, across the whole package, because
1997/// resolution has no type checker to ask what `holder.item` is a field of.
1998/// That over-approximates in one direction only: an unrelated struct's field
1999/// of the same name reads as opaque too, so a method called on it is reported
2000/// as dispatching dynamically when it does not. Naming a capability-open
2001/// declaration that is not one costs a reader a second look; missing one
2002/// costs them the guarantee.
2003#[derive(Debug, Default)]
2004struct OpaqueFields {
2005    /// Fields declared `dyn Trait`, or as one of the declaring struct's own
2006    /// generic parameters: reading one holds the opaque value itself.
2007    direct: BTreeSet<String>,
2008    /// Fields whose type only mentions such a value at depth, such as
2009    /// `entries: Array<dyn Summary>`: reading one holds an ordinary
2010    /// container, and what comes out of it is opaque.
2011    containers: BTreeSet<String>,
2012}
2013
2014impl OpaqueFields {
2015    /// Collects them from every struct the package declares.
2016    ///
2017    /// A struct's own generic parameters are what make `item: T` opaque, so
2018    /// each declaration is read against its own, not against the ones of
2019    /// whatever function later touches the field.
2020    fn of(package: &Package) -> Self {
2021        let mut fields = OpaqueFields::default();
2022        for module in package.modules.values() {
2023            for unit in &module.units {
2024                for item in &unit.ast.items {
2025                    let ItemKind::Struct(decl) = &item.kind else {
2026                        continue;
2027                    };
2028                    let generics: BTreeSet<String> = decl
2029                        .generics
2030                        .iter()
2031                        .map(|param| param.name.node.clone())
2032                        .collect();
2033                    for field in &decl.fields {
2034                        match type_opacity(&field.ty, &generics) {
2035                            Opacity::Direct => {
2036                                fields.direct.insert(field.name.node.clone());
2037                            }
2038                            Opacity::Container => {
2039                                fields.containers.insert(field.name.node.clone());
2040                            }
2041                            Opacity::None => {}
2042                        }
2043                    }
2044                }
2045            }
2046        }
2047        fields
2048    }
2049}
2050
2051/// How far out of this declaration's reach the implementation behind a value
2052/// was chosen.
2053///
2054/// The distinction the two non-`None` cases draw is the whole point: a
2055/// container of `dyn Trait` is itself an ordinary `Array`, so `items.length()`
2056/// is an ordinary call, while `items.get(0).summarize()` is not.
2057#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2058enum Opacity {
2059    /// An ordinary value: its type names no `dyn Trait` and no generic
2060    /// parameter.
2061    None,
2062    /// A container of opaque values. A method called on the container is an
2063    /// ordinary call; anything taken out of it is [`Opacity::Direct`].
2064    Container,
2065    /// The value itself is one whose implementation its producer chose: a
2066    /// `dyn Trait`, or a generic parameter of the declaration being walked.
2067    /// A method called on it runs a conformance picked where the value was
2068    /// made.
2069    Direct,
2070}
2071
2072/// Derives the Host API capabilities a function body calls directly, the raw
2073/// call sites found in it (used to build the module's call graph in a later
2074/// pass), and the indirect calls that make what it derived a lower bound.
2075///
2076/// This only looks at calls textually inside `decl`'s body (including nested
2077/// blocks, lambdas, match arms, loops, and local `fn` declarations). A
2078/// closure written here is walked here, which is the whole reason the lower
2079/// bound is worth having: a callback that prints is charged to the function
2080/// that *wrote* it, whatever later invokes it. `enums` is `None` here: a
2081/// module's enums are not all known yet at this point (see [`BodyWalk`]), so
2082/// `match` exhaustiveness is not checked during this walk.
2083///
2084/// The generic parameters this reads are `decl`'s own. An `impl` block's
2085/// generics are not consulted, because this is handed an [`FnDecl`] and
2086/// nothing else; that is not a hole today, since the parser rejects
2087/// `impl<T: Summary> Cell<T>`, but it becomes one the moment bounds on
2088/// `impl` blocks land.
2089fn analyze_body(
2090    decl: &FnDecl,
2091    host_uses: &BTreeSet<String>,
2092    host_items: &BTreeMap<String, String>,
2093    opaque_fields: &OpaqueFields,
2094    schemas: &HostSchemas,
2095) -> (BTreeSet<Capability>, Vec<CallShape>, BTreeSet<OpenCall>) {
2096    let generics: BTreeSet<String> = decl
2097        .generics
2098        .iter()
2099        .map(|param| param.name.node.clone())
2100        .collect();
2101    let mut walk = BodyWalk {
2102        host_uses,
2103        host_items,
2104        opaque_fields,
2105        schemas,
2106        enums: None,
2107        capabilities: BTreeSet::new(),
2108        calls: Vec::new(),
2109        errors: Vec::new(),
2110        warnings: Vec::new(),
2111        loop_depth: 0,
2112        generics,
2113        scopes: vec![Scope::default()],
2114        opaque: BTreeSet::new(),
2115        containers: BTreeSet::new(),
2116        open: BTreeSet::new(),
2117    };
2118    // `self` is a value the caller supplied, not a declaration this module
2119    // can be called through, so binding it keeps a body that merely reads it
2120    // from recording an edge to a same-named function.
2121    walk.bind_value("self");
2122    walk.bind_params(&decl.params);
2123    walk_block(&decl.body, &mut walk);
2124    (walk.capabilities, walk.calls, walk.open)
2125}
2126
2127/// Which [`Opacity`] a value of type `ty` has, for a declaration binding
2128/// `generics`.
2129fn type_opacity(ty: &cove_syntax::ast::Type, generics: &BTreeSet<String>) -> Opacity {
2130    if is_opaque_type(ty, generics) {
2131        Opacity::Direct
2132    } else if mentions_opaque_type(ty, generics) {
2133        Opacity::Container
2134    } else {
2135        Opacity::None
2136    }
2137}
2138
2139/// Whether a value of type `ty` is *itself* one whose implementation its
2140/// producer chose: a `dyn Trait`, or one of `generics` written bare.
2141///
2142/// This deliberately does not look inside a type's arguments.
2143/// `items: Array<T>` is an `Array` and nothing else, and `items.length()` is
2144/// `Array.length`, a builtin with no conformance to pick; only what comes out
2145/// of `items` is opaque. [`mentions_opaque_type`] is the question with the
2146/// depth in it.
2147fn is_opaque_type(ty: &cove_syntax::ast::Type, generics: &BTreeSet<String>) -> bool {
2148    use cove_syntax::ast::TypeKind;
2149    match &ty.kind {
2150        TypeKind::Dyn(_) => true,
2151        TypeKind::Named { path, .. } => path.len() == 1 && generics.contains(path[0].node.as_str()),
2152        TypeKind::Unit | TypeKind::Fn { .. } => false,
2153    }
2154}
2155
2156/// Whether `ty` names, at any depth, a value whose implementation its
2157/// producer chose rather than this declaration.
2158///
2159/// Depth matters because a container hands out its elements:
2160/// `entries: Array<dyn Summary>` is how a body comes to hold a `dyn Summary`
2161/// without ever writing the type again.
2162fn mentions_opaque_type(ty: &cove_syntax::ast::Type, generics: &BTreeSet<String>) -> bool {
2163    use cove_syntax::ast::TypeKind;
2164    match &ty.kind {
2165        TypeKind::Dyn(_) => true,
2166        TypeKind::Unit => false,
2167        TypeKind::Named { path, args } => {
2168            (path.len() == 1 && generics.contains(path[0].node.as_str()))
2169                || args.iter().any(|arg| mentions_opaque_type(arg, generics))
2170        }
2171        TypeKind::Fn {
2172            params,
2173            return_type,
2174            ..
2175        } => {
2176            params.iter().any(|param| {
2177                param
2178                    .ty
2179                    .as_ref()
2180                    .is_some_and(|ty| mentions_opaque_type(ty, generics))
2181            }) || return_type
2182                .as_ref()
2183                .is_some_and(|ty| mentions_opaque_type(ty, generics))
2184        }
2185    }
2186}
2187
2188/// Whether `expr`'s own value is one whose implementation its producer chose,
2189/// so that a method called on it runs a conformance picked somewhere this
2190/// call graph does not reach.
2191///
2192/// A bare name and a field read are values this walk can classify exactly:
2193/// `items: Array<T>` binds a container, so `items.length()` is an ordinary
2194/// call and `holder.entries.length()` is too, while `self.item.summarize()`
2195/// is not. Anything else — a call, an element taken out of a collection, a
2196/// chain — is a value with no name here, and [`mentions_opaque`] is what
2197/// decides it: without a type checker the walk cannot say what
2198/// `entries.get(0)` *is*, only that it came out of `entries`, which is enough
2199/// to know that a method called on it dispatches where this call graph does
2200/// not lead.
2201fn value_is_opaque(expr: &Expr, walk: &BodyWalk) -> bool {
2202    match &expr.kind {
2203        ExprKind::Ident(name) => walk.is_opaque(name),
2204        ExprKind::Field { base, name } => {
2205            walk.opaque_fields.direct.contains(name.node.as_str()) || value_is_opaque(base, walk)
2206        }
2207        _ => mentions_opaque(expr, walk),
2208    }
2209}
2210
2211/// Whether `expr` reads a name or a field bound to a *container* of opaque
2212/// values, so that a binding taken straight from it is a container too.
2213fn holds_opaque_container(expr: &Expr, walk: &BodyWalk) -> bool {
2214    match &expr.kind {
2215        ExprKind::Ident(name) => walk.is_container(name),
2216        ExprKind::Field { name, .. } => walk.opaque_fields.containers.contains(name.node.as_str()),
2217        _ => false,
2218    }
2219}
2220
2221/// The [`Opacity`] a `let` or `var` binding takes on.
2222///
2223/// A written type answers it outright. Without one this falls back to what
2224/// the initialiser *reads*, which is a mention rather than a type: a bare
2225/// name or a field read passes its own class along, and anything else that
2226/// touches an opaque value — `entries.get(0)`, `self.item.next()` — produces
2227/// the opaque value rather than a container of them.
2228///
2229/// What this cannot see is an initialiser whose opacity lives only in its
2230/// type. `let entry = makeDyn()` binds a `dyn Trait` that nothing in this
2231/// body writes, and resolution has no type checker to ask what `makeDyn`
2232/// returns, so the binding is not tracked and the dispatch on it falls back
2233/// to the receiver over-approximation. ADR 0015 names that gap.
2234fn binding_opacity(ty: Option<&cove_syntax::ast::Type>, value: &Expr, walk: &BodyWalk) -> Opacity {
2235    let written = ty.map_or(Opacity::None, |ty| type_opacity(ty, &walk.generics));
2236    let read = if value_is_opaque(value, walk) {
2237        Opacity::Direct
2238    } else if holds_opaque_container(value, walk) {
2239        Opacity::Container
2240    } else if mentions_opaque(value, walk) {
2241        Opacity::Direct
2242    } else {
2243        Opacity::None
2244    };
2245    written.max(read)
2246}
2247
2248/// Whether `expr` reads anything opaque, so that what it produces may be one
2249/// of those values or something taken out of one.
2250///
2251/// This is deliberately a mention rather than a type: without a type checker
2252/// the walk cannot say what `entries.get(0)` *is*, only that it came from
2253/// `entries`.
2254fn mentions_opaque(expr: &Expr, walk: &BodyWalk) -> bool {
2255    let any = |exprs: &[Expr]| exprs.iter().any(|e| mentions_opaque(e, walk));
2256    match &expr.kind {
2257        ExprKind::Ident(name) => walk.is_opaque(name) || walk.is_container(name),
2258        ExprKind::Field { base, name } => {
2259            walk.opaque_fields.direct.contains(name.node.as_str())
2260                || walk.opaque_fields.containers.contains(name.node.as_str())
2261                || mentions_opaque(base, walk)
2262        }
2263        ExprKind::Call {
2264            callee,
2265            args,
2266            trailing,
2267            ..
2268        } => {
2269            mentions_opaque(callee, walk)
2270                || args.iter().any(|arg| mentions_opaque(&arg.value, walk))
2271                || trailing
2272                    .as_ref()
2273                    .is_some_and(|tail| mentions_opaque(tail, walk))
2274        }
2275        ExprKind::Try(inner) | ExprKind::Await(inner) | ExprKind::Unary { operand: inner, .. } => {
2276            mentions_opaque(inner, walk)
2277        }
2278        ExprKind::Binary { lhs, rhs, .. } => {
2279            mentions_opaque(lhs, walk) || mentions_opaque(rhs, walk)
2280        }
2281        ExprKind::ArrayLit(items) => any(items),
2282        ExprKind::Str(parts) => parts.iter().any(|part| match part {
2283            StrPart::Interpolation(inner) => mentions_opaque(inner, walk),
2284            StrPart::Text(_) => false,
2285        }),
2286        ExprKind::Block(block) | ExprKind::Scope { body: block, .. } => {
2287            block_mentions_opaque(block, walk)
2288        }
2289        ExprKind::If {
2290            then_branch,
2291            else_branch,
2292            ..
2293        } => {
2294            block_mentions_opaque(then_branch, walk)
2295                || else_branch
2296                    .as_ref()
2297                    .is_some_and(|branch| mentions_opaque(branch, walk))
2298        }
2299        ExprKind::Match { arms, .. } => arms.iter().any(|arm| mentions_opaque(&arm.body, walk)),
2300        _ => false,
2301    }
2302}
2303
2304/// Whether a block's value can be an opaque one: its tail is what it
2305/// produces, and that is the only way a value leaves it.
2306///
2307/// A `break` inside the block is not a second way out. It belongs to an
2308/// enclosing loop rather than to this block, and a loop is not a value in
2309/// Cove anyway — `typeck` gives both `while` and `for` the type `Unit`
2310/// whatever a `break` inside them carries — so there is nothing here for a
2311/// `break` operand to become.
2312fn block_mentions_opaque(block: &Block, walk: &BodyWalk) -> bool {
2313    block
2314        .tail
2315        .as_ref()
2316        .is_some_and(|tail| mentions_opaque(tail, walk))
2317}
2318
2319/// The enums a module can name: the ones it declares, plus the ones it
2320/// imported, under the single name each is visible by.
2321type EnumsInScope<'a> = BTreeMap<&'a str, &'a EnumEntry>;
2322
2323/// Checks every `match` expression in every body of `program` for the
2324/// exhaustiveness and case-name facts derivable without a type checker, now
2325/// that every enum a module can name is known — including an imported one.
2326/// This walk also reports `break` and `continue` outside a loop, which does
2327/// not depend on `enums` and so already ran once (harmlessly, since
2328/// [`analyze_body`] discards its walk's errors) while the module's
2329/// declarations were being collected.
2330///
2331/// This reuses [`walk_block`] rather than a second traversal: the only
2332/// difference from the walk [`analyze_body`] already did is that `enums` is
2333/// filled in this time, so [`check_match_arms`] actually runs.
2334fn check_bodies(
2335    program: &Program,
2336    schemas: &HostSchemas,
2337    errors: &mut Vec<Diagnostic>,
2338    warnings: &mut Vec<Diagnostic>,
2339) {
2340    for resolved in program.modules.values() {
2341        let enums = enums_in_scope(program, resolved);
2342        for entry in resolved.functions.values() {
2343            check_body(
2344                &entry.decl.body,
2345                resolved,
2346                &enums,
2347                schemas,
2348                errors,
2349                warnings,
2350            );
2351        }
2352        for entry in resolved.methods.values() {
2353            // A default body belongs to the trait that declares it, so it is
2354            // walked once below rather than once per conformance — and in
2355            // the module that declares the trait, whose enums are the ones
2356            // its arms can name.
2357            if entry.from_trait_default.is_none() {
2358                check_body(
2359                    &entry.decl.body,
2360                    resolved,
2361                    &enums,
2362                    schemas,
2363                    errors,
2364                    warnings,
2365                );
2366            }
2367        }
2368        for entry in resolved.traits.values() {
2369            for method in &entry.decl.methods {
2370                if let Some(body) = &method.default {
2371                    check_body(body, resolved, &enums, schemas, errors, warnings);
2372                }
2373            }
2374        }
2375    }
2376}
2377
2378/// Every enum `resolved` can name, whether it declares it or imported it.
2379///
2380/// A `use` cannot bind a name the importing module declares, so the two
2381/// sources never disagree about a name.
2382fn enums_in_scope<'a>(program: &'a Program, resolved: &'a ResolvedModule) -> EnumsInScope<'a> {
2383    let mut enums: EnumsInScope<'a> = resolved
2384        .enums
2385        .iter()
2386        .map(|(name, entry)| (name.as_str(), entry))
2387        .collect();
2388    for (name, owner) in &resolved.imports {
2389        let Some(entry) = program
2390            .modules
2391            .get(owner)
2392            .and_then(|owner| owner.enums.get(name))
2393        else {
2394            continue;
2395        };
2396        enums.insert(name.as_str(), entry);
2397    }
2398    enums
2399}
2400
2401fn check_body(
2402    body: &Block,
2403    resolved: &ResolvedModule,
2404    enums: &EnumsInScope,
2405    schemas: &HostSchemas,
2406    errors: &mut Vec<Diagnostic>,
2407    warnings: &mut Vec<Diagnostic>,
2408) {
2409    let no_opaque_fields = OpaqueFields::default();
2410    let mut walk = BodyWalk {
2411        host_uses: &resolved.host_uses,
2412        host_items: &resolved.host_items,
2413        schemas,
2414        enums: Some(enums),
2415        capabilities: BTreeSet::new(),
2416        calls: Vec::new(),
2417        errors: Vec::new(),
2418        warnings: Vec::new(),
2419        loop_depth: 0,
2420        // This walk answers `match` questions; the capability facts it would
2421        // re-derive were already recorded by [`analyze_body`], so it starts
2422        // from no declaration and discards what it finds.
2423        generics: BTreeSet::new(),
2424        opaque_fields: &no_opaque_fields,
2425        scopes: Vec::new(),
2426        opaque: BTreeSet::new(),
2427        containers: BTreeSet::new(),
2428        open: BTreeSet::new(),
2429    };
2430    walk_block(body, &mut walk);
2431    errors.extend(walk.errors);
2432    warnings.extend(walk.warnings);
2433}
2434
2435/// Everything the body walker threads through a traversal, and what it
2436/// collects along the way.
2437///
2438/// The walker runs twice per body. The first run, from [`analyze_body`],
2439/// happens while a module's declarations are still being collected, so
2440/// `enums` is `None` and only `capabilities` and `calls` are derived. The
2441/// second run, from [`check_body_matches`], happens once every enum is
2442/// known; `enums` is filled in, and [`check_match_arms`] records what it
2443/// finds in `errors` and `warnings` instead.
2444struct BodyWalk<'a> {
2445    host_uses: &'a BTreeSet<String>,
2446    host_items: &'a BTreeMap<String, String>,
2447    /// The host modules this compilation can see, which is what turns a call
2448    /// into the capability it requires.
2449    schemas: &'a HostSchemas,
2450    enums: Option<&'a EnumsInScope<'a>>,
2451    capabilities: BTreeSet<Capability>,
2452    calls: Vec<CallShape>,
2453    errors: Vec<Diagnostic>,
2454    warnings: Vec<Diagnostic>,
2455    /// How many enclosing `for`/`while` loops the walk is currently inside,
2456    /// reset to `0` while walking a lambda body. `break` and `continue` only
2457    /// make sense inside a loop of the same function or closure; they cannot
2458    /// reach a loop outside a closure boundary, matching how `return` already
2459    /// only unwinds to the nearest enclosing call.
2460    loop_depth: u32,
2461    /// The generic parameters the declaration being walked binds, which is
2462    /// what makes `entry: T` a value whose implementation its caller chose
2463    /// rather than a value of a type named `T`.
2464    generics: BTreeSet<String>,
2465    /// Every field name in the package that holds, or contains, a value whose
2466    /// implementation its producer chose; see [`OpaqueFields`].
2467    opaque_fields: &'a OpaqueFields,
2468    /// The names this body has bound, innermost scope last.
2469    ///
2470    /// This exists so that a name the body binds is never mistaken for the
2471    /// module-level declaration it shadows: `fn label(report: String)` reads
2472    /// `report` as its own parameter, not as a call-graph edge to whatever
2473    /// `fn report` the module happens to declare.
2474    scopes: Vec<Scope>,
2475    /// The names bound to a value whose implementation its producer chose: a
2476    /// parameter or lambda parameter whose type is a `dyn Trait` or a generic
2477    /// parameter, and anything later bound from one by `let`, `var`, or
2478    /// `for`.
2479    ///
2480    /// Unlike [`BodyWalk::scopes`] this is flat and only grows. A name that
2481    /// has gone out of scope can then still read as opaque, which costs
2482    /// precision in one direction only — an extra `DynamicDispatch`, never a
2483    /// missing one — and in exchange a value that leaves a block through its
2484    /// tail keeps the class it was given inside.
2485    opaque: BTreeSet<String>,
2486    /// The same, for names bound to a *container* of such values, which is a
2487    /// different fact: `items.length()` is an ordinary call on an ordinary
2488    /// `Array`, and only what comes out of `items` is opaque.
2489    containers: BTreeSet<String>,
2490    /// Why what this walk derived is a lower bound; see [`OpenCall`].
2491    open: BTreeSet<OpenCall>,
2492}
2493
2494/// The names one lexical scope of a body binds.
2495#[derive(Debug, Default)]
2496struct Scope {
2497    /// Names bound to a value: parameters, `self`, `let` and `var` bindings,
2498    /// a `for` binding, a lambda's parameters. Calling one is a call to a
2499    /// value, and reading one is not a reference to a declaration.
2500    values: BTreeSet<String>,
2501    /// Local `fn` declarations. Their bodies are walked where they are
2502    /// written, exactly as a lambda's is, so calling one needs no call-graph
2503    /// edge and hides nothing.
2504    functions: BTreeSet<String>,
2505}
2506
2507impl BodyWalk<'_> {
2508    fn push_scope(&mut self) {
2509        self.scopes.push(Scope::default());
2510    }
2511
2512    fn pop_scope(&mut self) {
2513        self.scopes.pop();
2514    }
2515
2516    /// Records `name` as bound to a value in the innermost scope.
2517    fn bind_value(&mut self, name: &str) {
2518        if let Some(scope) = self.scopes.last_mut() {
2519            scope.values.insert(name.to_string());
2520        }
2521    }
2522
2523    /// Records `name` as a local `fn` in the innermost scope.
2524    fn bind_local_fn(&mut self, name: &str) {
2525        if let Some(scope) = self.scopes.last_mut() {
2526            scope.functions.insert(name.to_string());
2527        }
2528    }
2529
2530    /// Binds a declaration's or a lambda's parameters, with the opacity each
2531    /// one's written type gives it.
2532    ///
2533    /// A variadic parameter is an `Array` of what it was written as, so
2534    /// `items: T...` binds a container rather than an opaque value itself.
2535    fn bind_params(&mut self, params: &[cove_syntax::ast::Param]) {
2536        for param in params {
2537            let mut opacity = param
2538                .ty
2539                .as_ref()
2540                .map_or(Opacity::None, |ty| type_opacity(ty, &self.generics));
2541            if param.variadic {
2542                opacity = opacity.min(Opacity::Container);
2543            }
2544            self.bind_value(&param.name.node);
2545            self.mark(&param.name.node, opacity);
2546        }
2547    }
2548
2549    /// Records what `name` is bound to, when it is not an ordinary value.
2550    fn mark(&mut self, name: &str, opacity: Opacity) {
2551        match opacity {
2552            Opacity::None => {}
2553            Opacity::Container => {
2554                self.containers.insert(name.to_string());
2555            }
2556            Opacity::Direct => {
2557                self.opaque.insert(name.to_string());
2558            }
2559        }
2560    }
2561
2562    fn binds_value(&self, name: &str) -> bool {
2563        self.scopes.iter().any(|scope| scope.values.contains(name))
2564    }
2565
2566    fn binds_local_fn(&self, name: &str) -> bool {
2567        self.scopes
2568            .iter()
2569            .any(|scope| scope.functions.contains(name))
2570    }
2571
2572    fn is_opaque(&self, name: &str) -> bool {
2573        self.opaque.contains(name)
2574    }
2575
2576    fn is_container(&self, name: &str) -> bool {
2577        self.containers.contains(name)
2578    }
2579}
2580
2581/// The shape of one call site's callee, kept just precise enough to resolve
2582/// which declarations of the module it may reach. Resolution happens later,
2583/// once every declaration in the module is known; see [`resolve_calls`].
2584#[derive(Clone, Debug)]
2585enum CallShape {
2586    /// `f(...)`.
2587    Ident(String),
2588    /// `receiver.method(...)`. `receiver_ident` is the receiver's name when
2589    /// it is a bare identifier (such as `self` or a struct/enum name used as
2590    /// a namespace), and `None` for any other receiver expression.
2591    Field {
2592        receiver_ident: Option<String>,
2593        method: String,
2594    },
2595    /// A bare name read as a value rather than called: `handler: health`.
2596    ///
2597    /// A named function handed to a host, stored in a route table, or
2598    /// wrapped in a closure is called somewhere this body cannot see, so
2599    /// naming it is what makes it reachable at all. The edge is the same
2600    /// edge a call would make, which is what keeps a callback the host
2601    /// invokes from dropping out of the derived set.
2602    Reference(String),
2603}
2604
2605/// A node in a module's call graph: a free function or an `impl` method.
2606#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
2607pub enum FnKey {
2608    /// A free function, by its name.
2609    Fn(String),
2610    /// A method or associated function, by `(type name, function name)`.
2611    Method(String, String),
2612}
2613
2614fn walk_block(block: &Block, walk: &mut BodyWalk) {
2615    walk.push_scope();
2616    for stmt in &block.statements {
2617        walk_stmt(stmt, walk);
2618    }
2619    if let Some(tail) = &block.tail {
2620        walk_expr(tail, walk);
2621    }
2622    walk.pop_scope();
2623}
2624
2625fn walk_stmt(stmt: &Stmt, walk: &mut BodyWalk) {
2626    match &stmt.kind {
2627        StmtKind::Let {
2628            name, ty, value, ..
2629        } => {
2630            // The value is walked first, so `let x = x` still reads the
2631            // outer binding rather than the one it is about to make.
2632            walk_expr(value, walk);
2633            let opacity = binding_opacity(ty.as_ref(), value, walk);
2634            walk.bind_value(&name.node);
2635            walk.mark(&name.node, opacity);
2636        }
2637        StmtKind::Expr(expr) => walk_expr(expr, walk),
2638        // A local `fn` is an ordinary closure the enclosing body writes —
2639        // `typeck` and the interpreter both treat it as one — so it is
2640        // charged the same way an inline lambda is: its body is analysed
2641        // here, and calling it by name is an ordinary call rather than a
2642        // call to a value whose target nothing can name. Any other nested
2643        // declaration contributes nothing to this body.
2644        StmtKind::Item(item) => {
2645            if let ItemKind::Fn(decl) = &item.kind {
2646                walk.bind_local_fn(&decl.name.node);
2647                walk.push_scope();
2648                walk.bind_params(&decl.params);
2649                // A local `fn` is a closure boundary, so `break` and
2650                // `continue` inside it cannot reach a loop outside it, just
2651                // as they cannot out of a lambda.
2652                let outer_depth = std::mem::replace(&mut walk.loop_depth, 0);
2653                walk_block(&decl.body, walk);
2654                walk.loop_depth = outer_depth;
2655                walk.pop_scope();
2656            }
2657        }
2658    }
2659}
2660
2661fn walk_expr(expr: &Expr, walk: &mut BodyWalk) {
2662    match &expr.kind {
2663        ExprKind::Int(_)
2664        | ExprKind::Float(_)
2665        | ExprKind::Bool(_)
2666        | ExprKind::Duration(_)
2667        | ExprKind::Unit => {}
2668        // A name read as a value may be a function being handed somewhere
2669        // else to be called; see [`CallShape::Reference`]. A name this body
2670        // binds is not that function however it is spelled, so it records
2671        // nothing — otherwise a parameter or local shadowing a module-level
2672        // function would draw an exact edge to a declaration it cannot
2673        // reach. A name that turns out to be a type resolves to no
2674        // declaration and so records nothing either.
2675        ExprKind::Ident(name) => {
2676            if !walk.binds_value(name) && !walk.binds_local_fn(name) {
2677                walk.calls.push(CallShape::Reference(name.clone()));
2678            }
2679        }
2680        ExprKind::Str(parts) => {
2681            for part in parts {
2682                if let StrPart::Interpolation(inner) = part {
2683                    walk_expr(inner, walk);
2684                }
2685            }
2686        }
2687        ExprKind::ArrayLit(items) => {
2688            for item in items {
2689                walk_expr(item, walk);
2690            }
2691        }
2692        ExprKind::Field { base, .. } => walk_expr(base, walk),
2693        ExprKind::Call {
2694            callee,
2695            args,
2696            trailing,
2697            ..
2698        } => {
2699            if let Some(capability) =
2700                call_capability(callee, walk.host_uses, walk.host_items, walk.schemas)
2701            {
2702                walk.capabilities.insert(capability);
2703            }
2704            match call_shape(callee) {
2705                // A local `fn` is a closure this walk already analysed where
2706                // it was written, so calling it needs no edge and hides
2707                // nothing from the derived set.
2708                Some(CallShape::Ident(name)) if walk.binds_local_fn(&name) => {}
2709                // A name this body bound to a value is the higher-order
2710                // case whatever a module declares under the same name.
2711                Some(CallShape::Ident(name)) if walk.binds_value(&name) => {
2712                    walk.open.insert(OpenCall::FunctionValue);
2713                }
2714                Some(shape) => {
2715                    // A method call on a value whose implementation the
2716                    // caller chose runs a conformance picked where that
2717                    // value was made, which is not somewhere resolution can
2718                    // follow from here.
2719                    if let (CallShape::Field { .. }, ExprKind::Field { base: receiver, .. }) =
2720                        (&shape, &callee.kind)
2721                    {
2722                        if value_is_opaque(receiver, walk) {
2723                            walk.open.insert(OpenCall::DynamicDispatch);
2724                        }
2725                    }
2726                    walk.calls.push(shape);
2727                }
2728                // `handlers.get(0)()` and its neighbours: the callee is a
2729                // value with no name, so no edge leads to what it runs.
2730                None => {
2731                    walk.open.insert(OpenCall::FunctionValue);
2732                }
2733            }
2734            walk_expr(callee, walk);
2735            for arg in args {
2736                walk_expr(&arg.value, walk);
2737            }
2738            if let Some(trailing) = trailing {
2739                walk_expr(trailing, walk);
2740            }
2741        }
2742        ExprKind::Unary { operand, .. } => walk_expr(operand, walk),
2743        ExprKind::Binary { lhs, rhs, .. } => {
2744            walk_expr(lhs, walk);
2745            walk_expr(rhs, walk);
2746        }
2747        ExprKind::Assign { target, value, .. } => {
2748            walk_expr(target, walk);
2749            walk_expr(value, walk);
2750        }
2751        ExprKind::Try(inner) | ExprKind::Await(inner) => walk_expr(inner, walk),
2752        ExprKind::Block(block) => walk_block(block, walk),
2753        ExprKind::If {
2754            condition,
2755            then_branch,
2756            else_branch,
2757        } => {
2758            walk_expr(condition, walk);
2759            walk_block(then_branch, walk);
2760            if let Some(else_branch) = else_branch {
2761                walk_expr(else_branch, walk);
2762            }
2763        }
2764        ExprKind::Match { scrutinee, arms } => {
2765            walk_expr(scrutinee, walk);
2766            check_match_arms(expr, arms, walk);
2767            for MatchArm { body, .. } in arms {
2768                walk_expr(body, walk);
2769            }
2770        }
2771        ExprKind::For {
2772            binding,
2773            iterable,
2774            body,
2775        } => {
2776            walk_expr(iterable, walk);
2777            // Iterating a container of `dyn Trait` hands out one per turn,
2778            // so the binding is the opaque value the container held rather
2779            // than another container of them.
2780            let opaque = mentions_opaque(iterable, walk);
2781            walk.push_scope();
2782            walk.bind_value(&binding.node);
2783            if opaque {
2784                walk.mark(&binding.node, Opacity::Direct);
2785            }
2786            walk.loop_depth += 1;
2787            walk_block(body, walk);
2788            walk.loop_depth -= 1;
2789            walk.pop_scope();
2790        }
2791        ExprKind::While { condition, body } => {
2792            walk_expr(condition, walk);
2793            walk.loop_depth += 1;
2794            walk_block(body, walk);
2795            walk.loop_depth -= 1;
2796        }
2797        ExprKind::Return(inner) => {
2798            if let Some(inner) = inner {
2799                walk_expr(inner, walk);
2800            }
2801        }
2802        ExprKind::Break(inner) => {
2803            if let Some(inner) = inner {
2804                walk_expr(inner, walk);
2805            }
2806            check_in_loop(expr, "break", walk);
2807        }
2808        ExprKind::Continue => check_in_loop(expr, "continue", walk),
2809        // A lambda is a separate closure boundary: `break` and `continue`
2810        // cannot reach a loop outside it, exactly as `return` inside a
2811        // lambda returns from the lambda, not the enclosing function.
2812        ExprKind::Lambda { params, body, .. } => {
2813            let outer_depth = std::mem::replace(&mut walk.loop_depth, 0);
2814            walk.push_scope();
2815            walk.bind_params(params);
2816            walk_block(body, walk);
2817            walk.pop_scope();
2818            walk.loop_depth = outer_depth;
2819        }
2820        ExprKind::Scope { body, .. } => walk_block(body, walk),
2821        ExprKind::Range { start, end, .. } => {
2822            walk_expr(start, walk);
2823            walk_expr(end, walk);
2824        }
2825    }
2826}
2827
2828/// Reports `keyword` (`break` or `continue`) used outside any loop this walk
2829/// has entered. A lambda body resets [`BodyWalk::loop_depth`] to `0`, so this
2830/// also rejects reaching for a loop across a closure boundary.
2831fn check_in_loop(expr: &Expr, keyword: &str, walk: &mut BodyWalk) {
2832    if walk.loop_depth == 0 {
2833        walk.errors.push(
2834            Diagnostic::error(
2835                format!("cove::resolve::{keyword}_outside_loop"),
2836                format!("`{keyword}` outside a loop"),
2837            )
2838            .at(expr.span)
2839            .rule(format!(
2840                "`{keyword}` only makes sense inside a `for` or `while` loop, and cannot reach one outside a closure."
2841            ))
2842            .help(format!("move this `{keyword}` inside an enclosing loop, or remove it")),
2843        );
2844    }
2845}
2846
2847/// Checks one `match` expression for the exhaustiveness and case-name facts
2848/// derivable without a type checker. A no-op until every enum in the module
2849/// is known (`walk.enums.is_some()`); see [`BodyWalk`].
2850///
2851/// The scrutinee's enum is determined from the arms' `Variant` patterns
2852/// alone (see [`resolve_target_enum`]); when it cannot be determined, this
2853/// silently reports nothing rather than guess. `Wildcard` and `Binding` arms
2854/// make a match exhaustive by construction, since there is no static type to
2855/// check them against. Arms after the first such catch-all arm can never
2856/// run, which is checked independently of whether the enum could be
2857/// determined at all.
2858fn check_match_arms(match_expr: &Expr, arms: &[MatchArm], walk: &mut BodyWalk) {
2859    let Some(enums) = walk.enums else {
2860        return;
2861    };
2862
2863    let catch_all_index = arms.iter().position(|arm| is_catch_all(&arm.pattern));
2864    if let Some(catch_all_index) = catch_all_index {
2865        let catch_all_span = arms[catch_all_index].span;
2866        for arm in &arms[catch_all_index + 1..] {
2867            walk.warnings.push(
2868                Diagnostic::warning(
2869                    "cove::resolve::unreachable_match_arm",
2870                    "this `match` arm can never run",
2871                )
2872                .at(arm.span)
2873                .label(
2874                    catch_all_span,
2875                    "unreachable because this earlier arm matches everything",
2876                )
2877                .rule("An arm after a `_` or binding arm can never run."),
2878            );
2879        }
2880    }
2881    let has_catch_all = catch_all_index.is_some();
2882
2883    if let Some(target) = resolve_target_enum(arms, enums) {
2884        let valid_cases = target.case_names();
2885        // Which case each arm names, for exhaustiveness. A case an arm
2886        // names is covered whatever its sub-patterns are, which is what
2887        // this pass has always answered and is not what changes below.
2888        let mut seen: BTreeMap<&str, Span> = BTreeMap::new();
2889        // Every arm that could still be reached when it was read, for
2890        // [`pattern_covers`] to compare a later arm against.
2891        let mut reachable: Vec<&Pattern> = Vec::new();
2892        for arm in arms {
2893            let PatternKind::Variant { path, .. } = &arm.pattern.kind else {
2894                continue;
2895            };
2896            let case_name = path.last().expect("a variant path is never empty");
2897            if !valid_cases.iter().any(|case| case == &case_name.node) {
2898                walk.errors.push(
2899                    Diagnostic::error(
2900                        "cove::resolve::unknown_enum_case",
2901                        format!(
2902                            "`{}` is not a case of `{}`",
2903                            case_name.node,
2904                            target.display_name()
2905                        ),
2906                    )
2907                    .at(arm.pattern.span)
2908                    .rule("Every `match` arm must name a case its enum declares.")
2909                    .help(format!(
2910                        "`{}` declares {}",
2911                        target.display_name(),
2912                        list_backticked(&valid_cases)
2913                    )),
2914                );
2915                continue;
2916            }
2917            let covering = reachable
2918                .iter()
2919                .find(|earlier| pattern_covers(earlier, &arm.pattern));
2920            if let Some(covering) = covering {
2921                walk.errors.push(
2922                    Diagnostic::error(
2923                        "cove::resolve::duplicate_match_arm",
2924                        format!(
2925                            "this `{}` arm is already covered by an earlier arm",
2926                            case_name.node
2927                        ),
2928                    )
2929                    .at(arm.pattern.span)
2930                    .label(
2931                        covering.span,
2932                        "this earlier arm matches every value it would",
2933                    )
2934                    .rule("A `match` arm must match some value no earlier arm matches."),
2935                );
2936            } else {
2937                reachable.push(&arm.pattern);
2938            }
2939            seen.entry(case_name.node.as_str())
2940                .or_insert(arm.pattern.span);
2941        }
2942
2943        if !has_catch_all {
2944            let missing: Vec<String> = valid_cases
2945                .iter()
2946                .filter(|case| !seen.contains_key(case.as_str()))
2947                .map(|case| target.qualified(case))
2948                .collect();
2949            if !missing.is_empty() {
2950                walk.errors.push(non_exhaustive_enum_match(
2951                    match_expr.span,
2952                    &target,
2953                    &missing,
2954                ));
2955            }
2956        }
2957        return;
2958    }
2959
2960    check_literal_arms(match_expr, arms, catch_all_index, has_catch_all, walk);
2961}
2962
2963/// Checks a `match`'s literal-pattern arms once no enum could be determined
2964/// for the scrutinee (see [`resolve_target_enum`]).
2965///
2966/// `Bool` is exhaustible in a way `Int` and `String` are not: its domain is
2967/// exactly two values, `true` and `false`, so a `match` whose literal arms
2968/// are all `Bool` can be proven exhaustive once both are covered, with no
2969/// catch-all required. This is a property of the type, not a special case
2970/// carved out for `match` — `Int` and `String` have effectively unbounded
2971/// domains, so a literal `match` over either can never be proven exhaustive
2972/// without a catch-all. A match mixing a `Bool` literal with a non-`Bool`
2973/// literal (which cannot happen once there is a type checker, but nothing
2974/// here rules it out yet) is treated as the non-`Bool` case, since it still
2975/// needs a catch-all.
2976fn check_literal_arms(
2977    match_expr: &Expr,
2978    arms: &[MatchArm],
2979    catch_all_index: Option<usize>,
2980    has_catch_all: bool,
2981    walk: &mut BodyWalk,
2982) {
2983    let literal_indices: Vec<usize> = arms
2984        .iter()
2985        .enumerate()
2986        .filter(|(_, arm)| is_literal(&arm.pattern))
2987        .map(|(index, _)| index)
2988        .collect();
2989    if literal_indices.is_empty() {
2990        return;
2991    }
2992
2993    let all_bool = literal_indices
2994        .iter()
2995        .all(|&index| literal_bool_value(&arms[index].pattern).is_some());
2996
2997    if !all_bool {
2998        if !has_catch_all {
2999            walk.errors.push(
3000                Diagnostic::error(
3001                    "cove::resolve::non_exhaustive_match",
3002                    "`match` over literal patterns needs a `_` or binding arm",
3003                )
3004                .at(match_expr.span)
3005                .rule("`match` must cover every enum case.")
3006                .help(
3007                    "add a `_` arm, or a binding arm, to cover every value the literal arms do not",
3008                ),
3009            );
3010        }
3011        return;
3012    }
3013
3014    let mut seen: BTreeMap<bool, Span> = BTreeMap::new();
3015    let mut covered_at: Option<usize> = None;
3016    for &index in &literal_indices {
3017        let value = literal_bool_value(&arms[index].pattern).expect("checked all_bool above");
3018        if let Some(first_span) = seen.get(&value) {
3019            walk.errors.push(
3020                Diagnostic::error(
3021                    "cove::resolve::duplicate_match_arm",
3022                    format!("`{value}` is already covered by an earlier arm"),
3023                )
3024                .at(arms[index].pattern.span)
3025                .label(*first_span, format!("`{value}` first matched here"))
3026                .rule("Each value of `Bool` may be matched by at most one arm."),
3027            );
3028            continue;
3029        }
3030        seen.insert(value, arms[index].pattern.span);
3031        if seen.len() == 2 && covered_at.is_none() {
3032            covered_at = Some(index);
3033        }
3034    }
3035
3036    if let Some(catch_all_index) = catch_all_index {
3037        if let Some(covered_at) = covered_at {
3038            if covered_at < catch_all_index {
3039                walk.warnings.push(
3040                    Diagnostic::warning(
3041                        "cove::resolve::unreachable_match_arm",
3042                        "this `match` arm can never run",
3043                    )
3044                    .at(arms[catch_all_index].span)
3045                    .label(
3046                        arms[covered_at].span,
3047                        "unreachable because `true` and `false` are already covered here",
3048                    )
3049                    .rule("A `match` over `Bool` covering both `true` and `false` leaves no value for a later `_` or binding arm."),
3050                );
3051            }
3052        }
3053        return;
3054    }
3055
3056    if seen.len() < 2 {
3057        let missing = if seen.contains_key(&true) {
3058            "false"
3059        } else {
3060            "true"
3061        };
3062        walk.errors.push(
3063            Diagnostic::error(
3064                "cove::resolve::non_exhaustive_match",
3065                format!("this `match` does not cover `{missing}`"),
3066            )
3067            .at(match_expr.span)
3068            .rule("A `match` over `Bool` must cover both `true` and `false`.")
3069            .help(format!("add a `{missing} => ...` arm, or add a `_` arm")),
3070        );
3071    }
3072}
3073
3074/// The `Bool` value a literal pattern matches, or `None` when the pattern is
3075/// not a literal or its literal is not a `Bool`.
3076fn literal_bool_value(pattern: &Pattern) -> Option<bool> {
3077    let PatternKind::Literal(expr) = &pattern.kind else {
3078        return None;
3079    };
3080    match expr.kind {
3081        ExprKind::Bool(value) => Some(value),
3082        _ => None,
3083    }
3084}
3085
3086/// Whether `earlier` matches every value `later` matches.
3087///
3088/// This is the question behind `cove::resolve::duplicate_match_arm`. A
3089/// second `None =>` after a first is dead code the author did not mean to
3090/// write, and so is a second `Some(other)` after a first, because a binding
3091/// sub-pattern is a catch-all for its case. `Some(Json.Text(value))` is
3092/// not: it matches only a `Some` holding a `Json.Text`, so the arm after it
3093/// is reachable. Naming the case is therefore not the test — covering every
3094/// value the later arm would take is — and the test recurses, because a
3095/// sub-pattern covers a sub-pattern by the same rule.
3096///
3097/// The rules are the two backends' matching rules, read as a question about
3098/// patterns rather than about a value:
3099///
3100/// - `_` and a binding match everything, so either covers anything, and
3101///   only they cover a binding.
3102/// - A variant pattern covers one naming the same case whose sub-patterns
3103///   it covers one for one. A pattern that writes fewer sub-patterns than
3104///   another tests fewer payload slots, and a slot it does not test is one
3105///   it matches, so the missing ones read as `_`.
3106/// - A literal covers an equal literal.
3107///
3108/// The case is compared by its last path segment, which is what the arm
3109/// loop above compares and what the lowering tests.
3110///
3111/// Two answers here are deliberately "no" rather than a guess. A literal
3112/// pattern is a literal token *or* a `-` applied to any expression, and
3113/// what `-n` matches depends on what the enclosing scope holds each time
3114/// the arm is tried, so two of them are not the same pattern in any sense
3115/// this can decide; and a `Float` literal is left alone because equality is
3116/// not the relation to refuse a program on there.
3117///
3118/// This compares one arm against one earlier arm, never against several
3119/// together. `Some(other)` after `Some(Json.Text(t))` and
3120/// `Some(Json.Number(n))` is unreachable only if `Json` declares no third
3121/// case, and that is exhaustiveness over the payload, which this pass does
3122/// not prove.
3123fn pattern_covers(earlier: &Pattern, later: &Pattern) -> bool {
3124    match (&earlier.kind, &later.kind) {
3125        (PatternKind::Wildcard | PatternKind::Binding(_), _) => true,
3126        (_, PatternKind::Wildcard | PatternKind::Binding(_)) => false,
3127        (PatternKind::Literal(earlier), PatternKind::Literal(later)) => {
3128            same_literal(earlier, later)
3129        }
3130        (
3131            PatternKind::Variant {
3132                path: earlier_path,
3133                payload: earlier_payload,
3134            },
3135            PatternKind::Variant {
3136                path: later_path,
3137                payload: later_payload,
3138            },
3139        ) => {
3140            let earlier_case = earlier_path.last().expect("a variant path is never empty");
3141            let later_case = later_path.last().expect("a variant path is never empty");
3142            earlier_case.node == later_case.node
3143                && (0..earlier_payload.len().max(later_payload.len()))
3144                    .all(|slot| covers_slot(earlier_payload.get(slot), later_payload.get(slot)))
3145        }
3146        _ => false,
3147    }
3148}
3149
3150/// [`pattern_covers`] for one payload slot, where either side may not have
3151/// written a sub-pattern for it. A slot a pattern does not test is one it
3152/// matches, so an absent sub-pattern reads as `_`.
3153fn covers_slot(earlier: Option<&Pattern>, later: Option<&Pattern>) -> bool {
3154    match (earlier, later) {
3155        (None, _) => true,
3156        (Some(earlier), None) => is_catch_all(earlier),
3157        (Some(earlier), Some(later)) => pattern_covers(earlier, later),
3158    }
3159}
3160
3161/// Whether two literal patterns are the same literal, for the token forms
3162/// where that is a question about the program rather than about what the
3163/// enclosing scope holds when the arm is tried. Everything else — a
3164/// `Float`, a `-` applied to an expression, an interpolated string —
3165/// answers `false`; see [`pattern_covers`].
3166fn same_literal(earlier: &Expr, later: &Expr) -> bool {
3167    match (&earlier.kind, &later.kind) {
3168        (ExprKind::Int(earlier), ExprKind::Int(later)) => earlier == later,
3169        (ExprKind::Bool(earlier), ExprKind::Bool(later)) => earlier == later,
3170        (ExprKind::Duration(earlier), ExprKind::Duration(later)) => earlier == later,
3171        (ExprKind::Str(earlier), ExprKind::Str(later)) => {
3172            match (plain_text(earlier), plain_text(later)) {
3173                (Some(earlier), Some(later)) => earlier == later,
3174                _ => false,
3175            }
3176        }
3177        _ => false,
3178    }
3179}
3180
3181/// The text of a string literal that interpolates nothing, or `None` when
3182/// it does interpolate: what an interpolation produces is not known here.
3183fn plain_text(parts: &[StrPart]) -> Option<String> {
3184    let mut text = String::new();
3185    for part in parts {
3186        match part {
3187            StrPart::Text(chunk) => text.push_str(chunk),
3188            StrPart::Interpolation(_) => return None,
3189        }
3190    }
3191    Some(text)
3192}
3193
3194fn is_catch_all(pattern: &Pattern) -> bool {
3195    matches!(
3196        pattern.kind,
3197        PatternKind::Wildcard | PatternKind::Binding(_)
3198    )
3199}
3200
3201fn is_literal(pattern: &Pattern) -> bool {
3202    matches!(pattern.kind, PatternKind::Literal(_))
3203}
3204
3205/// The enum a `match`'s `Variant` arms name, when this analysis can
3206/// determine it. `Option` and `Result` are builtins rather than module
3207/// declarations, so they are represented separately from a module `enum`.
3208enum TargetEnum<'a> {
3209    Declared(&'a EnumEntry),
3210    /// One of the language's own enums, held as the schema entry that
3211    /// declares it. What its cases are is a question this crate asks rather
3212    /// than answers: `cove_schema::builtins` says that an `Option` is `Some`
3213    /// and `None`, and the runtime builds those values out of the same
3214    /// entry.
3215    Builtin(&'static cove_schema::builtins::BuiltinSchema),
3216}
3217
3218impl TargetEnum<'_> {
3219    fn display_name(&self) -> &str {
3220        match self {
3221            TargetEnum::Declared(entry) => &entry.decl.name.node,
3222            TargetEnum::Builtin(schema) => schema.name,
3223        }
3224    }
3225
3226    /// Case names in declaration order.
3227    fn case_names(&self) -> Vec<String> {
3228        match self {
3229            TargetEnum::Declared(entry) => entry
3230                .decl
3231                .cases
3232                .iter()
3233                .map(|case| case.name.node.clone())
3234                .collect(),
3235            TargetEnum::Builtin(schema) => schema
3236                .cases
3237                .iter()
3238                .map(|case| case.name.to_string())
3239                .collect(),
3240        }
3241    }
3242
3243    /// How a missing case should read in a diagnostic: qualified for a
3244    /// module enum (`LogLevel.Warn`), bare for a builtin, since arms write
3245    /// `Some(x)` and `None`, never `Option.Some(x)`.
3246    fn qualified(&self, case: &str) -> String {
3247        match self {
3248            TargetEnum::Declared(entry) => format!("{}.{case}", entry.decl.name.node),
3249            TargetEnum::Builtin(_) => case.to_string(),
3250        }
3251    }
3252}
3253
3254/// Determines the enum a `match`'s `Variant` arms name, or `None` when this
3255/// analysis cannot be sure: no arm is a `Variant`, the arms disagree about
3256/// which enum they name, a bare case name matches no enum in scope or more
3257/// than one, or the settled-on enum is one this module can neither declare
3258/// nor name through an import.
3259///
3260/// A path of three or more segments, such as the `booking.Status.Confirmed`
3261/// an imported *module* makes writable, still abstains: the arms carry no
3262/// type, so the enum a qualified path names is left to the type checker.
3263fn resolve_target_enum<'a>(arms: &[MatchArm], enums: &EnumsInScope<'a>) -> Option<TargetEnum<'a>> {
3264    let mut candidate: Option<String> = None;
3265    for arm in arms {
3266        let PatternKind::Variant { path, .. } = &arm.pattern.kind else {
3267            continue;
3268        };
3269        let this_enum = match path.as_slice() {
3270            [case] => bare_case_enum(&case.node, enums)?,
3271            [enum_name, _case] => enum_name.node.clone(),
3272            _ => return None,
3273        };
3274        match &candidate {
3275            None => candidate = Some(this_enum),
3276            Some(existing) if *existing != this_enum => return None,
3277            _ => {}
3278        }
3279    }
3280
3281    let candidate = candidate?;
3282    match cove_schema::builtin(&candidate) {
3283        Some(schema) if schema.is_enum() => Some(TargetEnum::Builtin(schema)),
3284        _ => enums
3285            .get(candidate.as_str())
3286            .copied()
3287            .map(TargetEnum::Declared),
3288    }
3289}
3290
3291/// The enum a bare case name such as `Debug` names: the builtin enum that
3292/// declares it, or the one enum in scope whose cases include it. `None` when
3293/// no enum declares that case, or more than one does.
3294fn bare_case_enum(case_name: &str, enums: &EnumsInScope) -> Option<String> {
3295    if let Some(schema) = cove_schema::builtins::enum_declaring(case_name) {
3296        return Some(schema.name.to_string());
3297    }
3298    let mut matches = enums
3299        .iter()
3300        .filter(|(_, entry)| {
3301            entry
3302                .decl
3303                .cases
3304                .iter()
3305                .any(|case| case.name.node == case_name)
3306        })
3307        .map(|(name, _)| (*name).to_string());
3308    let first = matches.next()?;
3309    if matches.next().is_some() {
3310        return None;
3311    }
3312    Some(first)
3313}
3314
3315fn list_backticked(items: &[String]) -> String {
3316    items
3317        .iter()
3318        .map(|item| format!("`{item}`"))
3319        .collect::<Vec<_>>()
3320        .join(", ")
3321}
3322
3323/// Builds the `non_exhaustive_match` diagnostic for a `match` over `target`
3324/// missing the cases in `missing`, in declaration order.
3325fn non_exhaustive_enum_match(span: Span, target: &TargetEnum, missing: &[String]) -> Diagnostic {
3326    let list = list_backticked(missing);
3327    let help = if missing.len() == 1 {
3328        format!("add an arm for {list}, or add a `_` arm")
3329    } else {
3330        format!("add arms for {list}, or add a `_` arm")
3331    };
3332    Diagnostic::error(
3333        "cove::resolve::non_exhaustive_match",
3334        format!(
3335            "`match` does not cover every case of `{}`: missing {list}",
3336            target.display_name()
3337        ),
3338    )
3339    .at(span)
3340    .rule("`match` must cover every enum case.")
3341    .help(help)
3342}
3343
3344/// The call-graph shape of `callee`, when it is a form the call graph can
3345/// resolve (a bare name or a field access). Any other callee, such as an
3346/// immediately-called lambda or an element taken out of a collection,
3347/// contributes no call-graph edge; its caller records
3348/// [`OpenCall::FunctionValue`] instead, so the gap is reported rather than
3349/// dropped.
3350///
3351/// A field that holds a function value is a hole waiting to open. `h.cb()`
3352/// and `(h.cb)()` on a `fn`-typed field are rejected by the type checker
3353/// today, so there is nothing to miss — but the parser normalises both into
3354/// a `Field` callee, which this reads as a method call, resolves to no
3355/// method, and marks nothing. The day a function-typed field becomes callable
3356/// this has to distinguish the two.
3357fn call_shape(callee: &Expr) -> Option<CallShape> {
3358    match &callee.kind {
3359        ExprKind::Ident(name) => Some(CallShape::Ident(name.clone())),
3360        ExprKind::Field { base, name } => {
3361            let receiver_ident = match &base.kind {
3362                ExprKind::Ident(base_name) => Some(base_name.clone()),
3363                _ => None,
3364            };
3365            Some(CallShape::Field {
3366                receiver_ident,
3367                method: name.node.clone(),
3368            })
3369        }
3370        _ => None,
3371    }
3372}
3373
3374/// One node of the package's call graph: a declaration, and the module that
3375/// declares it.
3376pub type Node = (String, FnKey);
3377
3378/// How precisely a call site named the callee an edge leads to.
3379///
3380/// Both kinds of edge are sound for the capability fixed point, which only
3381/// ever unions more in. They differ for anything that reports an edge to a
3382/// person: an approximate edge may not exist in any real execution.
3383#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
3384pub enum CallPrecision {
3385    /// The call site names the callee: a free function, a module-qualified
3386    /// export, or a method of a receiver whose type is written at the call
3387    /// site.
3388    Exact,
3389    /// The receiver's type is not known without a type checker, so the call
3390    /// site was resolved to every same-named method reachable through
3391    /// imports. See [`FnEntry::required_capabilities`].
3392    Approximate,
3393}
3394
3395/// Resolves every call site recorded while the modules were resolved to the
3396/// declarations it may reach, anywhere in the package.
3397///
3398/// The second map is what resolution could *not* do: the call sites whose
3399/// callee is a value rather than a declaration, keyed by the declaration
3400/// that wrote them. It is merged into each entry's `direct_open_calls`
3401/// before the fixed point runs, so a lower bound and the reason it is one
3402/// travel together.
3403#[allow(clippy::type_complexity)]
3404fn package_call_graph(
3405    program: &Program,
3406    call_sites: &BTreeMap<Node, Vec<CallShape>>,
3407) -> (
3408    BTreeMap<Node, BTreeMap<Node, CallPrecision>>,
3409    BTreeMap<Node, BTreeSet<OpenCall>>,
3410) {
3411    let reachable: BTreeMap<&str, BTreeSet<&str>> = program
3412        .modules
3413        .keys()
3414        .map(|name| (name.as_str(), reachable_modules(program, name)))
3415        .collect();
3416    let mut graph = BTreeMap::new();
3417    let mut open = BTreeMap::new();
3418    for ((module, key), calls) in call_sites {
3419        let (targets, unresolved) =
3420            resolve_calls(program, module, calls, &reachable[module.as_str()]);
3421        let node = (module.clone(), key.clone());
3422        if !unresolved.is_empty() {
3423            open.insert(node.clone(), unresolved);
3424        }
3425        graph.insert(node, targets);
3426    }
3427    (graph, open)
3428}
3429
3430/// Records what resolution could not follow on the declarations that wrote
3431/// it, beside what each already found in its own body.
3432fn merge_open_calls(program: &mut Program, unresolved: &BTreeMap<Node, BTreeSet<OpenCall>>) {
3433    for (module, resolved) in program.modules.iter_mut() {
3434        for (name, entry) in resolved.functions.iter_mut() {
3435            if let Some(open) = unresolved.get(&(module.clone(), FnKey::Fn(name.clone()))) {
3436                entry.direct_open_calls.extend(open.iter().copied());
3437            }
3438        }
3439        for ((type_name, method_name), entry) in resolved.methods.iter_mut() {
3440            let key = FnKey::Method(type_name.clone(), method_name.clone());
3441            if let Some(open) = unresolved.get(&(module.clone(), key)) {
3442                entry.direct_open_calls.extend(open.iter().copied());
3443            }
3444        }
3445    }
3446}
3447
3448/// Every module `module` can reach through imports, directly or through the
3449/// modules it imports, including itself.
3450///
3451/// This is the set a value's type can be declared by where `module` runs: a
3452/// declaration this module never mentions still arrives here through
3453/// something it does import. The walk terminates because it visits each
3454/// module once, so a cycle that resolution is about to reject cannot spin it.
3455fn reachable_modules<'a>(program: &'a Program, module: &'a str) -> BTreeSet<&'a str> {
3456    let mut reached: BTreeSet<&str> = BTreeSet::new();
3457    let mut pending: Vec<&str> = vec![module];
3458    while let Some(name) = pending.pop() {
3459        if !reached.insert(name) {
3460            continue;
3461        }
3462        if let Some(resolved) = program.modules.get(name) {
3463            pending.extend(resolved.dependencies());
3464        }
3465    }
3466    reached
3467}
3468
3469/// Resolves the raw call sites found in one declaration's body to the
3470/// declarations they may call, in `module` or in any module `module` imports
3471/// from.
3472///
3473/// A bare-name call resolves to the free function of that name the calling
3474/// module declares, or, failing that, to the one it imported under that
3475/// name. A call qualified by an imported module (`booking.create(...)`)
3476/// resolves to that module's exported function. A field-access call whose
3477/// receiver is a bare identifier naming a struct or enum in scope — declared
3478/// here or imported — resolves precisely to that type's method, in the
3479/// module that declares the type.
3480///
3481/// Every other field-access call — a receiver that is `self`, a local
3482/// variable, or any other expression whose type is unknown without a type
3483/// checker — resolves to *every* method sharing that name in `reachable`,
3484/// the modules this one can reach through imports. That is a deliberate
3485/// over-approximation: it can name a capability a call site does not really
3486/// reach, but never misses one. `reachable` is transitive rather than
3487/// direct because a value can be declared by a module this one never
3488/// mentions and still arrive here, as the result of something it does
3489/// import.
3490///
3491/// A bare name that resolves to no declaration at all is the higher-order
3492/// case: `work()` where `work` is a parameter or a local. There is nothing
3493/// to draw an edge to, so the call site is reported as
3494/// [`OpenCall::FunctionValue`] in the second return value instead of
3495/// vanishing.
3496fn resolve_calls(
3497    program: &Program,
3498    module: &str,
3499    calls: &[CallShape],
3500    reachable: &BTreeSet<&str>,
3501) -> (BTreeMap<Node, CallPrecision>, BTreeSet<OpenCall>) {
3502    let Some(resolved) = program.modules.get(module) else {
3503        return (BTreeMap::new(), BTreeSet::new());
3504    };
3505    let mut targets: BTreeMap<Node, CallPrecision> = BTreeMap::new();
3506    let mut open: BTreeSet<OpenCall> = BTreeSet::new();
3507    for call in calls {
3508        match call {
3509            CallShape::Reference(name) => {
3510                if resolved.functions.contains_key(name) {
3511                    exact(&mut targets, (module.to_string(), FnKey::Fn(name.clone())));
3512                } else if let Some(owner) = declaring_module(program, resolved, name, |owner| {
3513                    owner.functions.contains_key(name)
3514                }) {
3515                    exact(&mut targets, (owner, FnKey::Fn(name.clone())));
3516                }
3517            }
3518            CallShape::Ident(name) => {
3519                if resolved.functions.contains_key(name) {
3520                    exact(&mut targets, (module.to_string(), FnKey::Fn(name.clone())));
3521                } else if let Some(owner) = declaring_module(program, resolved, name, |owner| {
3522                    owner.functions.contains_key(name)
3523                }) {
3524                    exact(&mut targets, (owner, FnKey::Fn(name.clone())));
3525                } else if calls_a_value(program, resolved, name) {
3526                    open.insert(OpenCall::FunctionValue);
3527                }
3528            }
3529            CallShape::Field {
3530                receiver_ident,
3531                method,
3532            } => {
3533                let owner = receiver_ident.as_ref().and_then(|head| {
3534                    declaring_module(program, resolved, head, |owner| {
3535                        owner.structs.contains_key(head) || owner.enums.contains_key(head)
3536                    })
3537                    .map(|owner| (owner, head.clone()))
3538                });
3539                if let Some((owner, type_name)) = owner {
3540                    for node in type_methods(program, &owner, &type_name, method) {
3541                        exact(&mut targets, node);
3542                    }
3543                    continue;
3544                }
3545                if let Some(target) = receiver_ident
3546                    .as_ref()
3547                    .and_then(|head| resolved.module_imports.get(head))
3548                {
3549                    if let Some(owner) = program.modules.get(target) {
3550                        if owner
3551                            .functions
3552                            .get(method)
3553                            .is_some_and(|entry| entry.exported)
3554                        {
3555                            exact(&mut targets, (target.clone(), FnKey::Fn(method.clone())));
3556                            continue;
3557                        }
3558                    }
3559                }
3560                for name in reachable {
3561                    let Some(candidate) = program.modules.get(*name) else {
3562                        continue;
3563                    };
3564                    for (type_name, method_name) in candidate.methods.keys() {
3565                        if method_name == method {
3566                            targets
3567                                .entry((
3568                                    (*name).to_string(),
3569                                    FnKey::Method(type_name.clone(), method_name.clone()),
3570                                ))
3571                                .or_insert(CallPrecision::Approximate);
3572                        }
3573                    }
3574                }
3575            }
3576        }
3577    }
3578    (targets, open)
3579}
3580
3581/// Whether a bare `name(...)` that resolved to no function is a call to a
3582/// value a parameter or local holds.
3583///
3584/// A bare call is one of a short list of things — a declared function, a
3585/// struct initializer, a host item a `use console.println` brought into
3586/// scope, a builtin written without a receiver such as `Ok` or `assert`, a
3587/// builtin type used as a namespace, or a value. Only the last is indirect,
3588/// so everything else is ruled out by name before the call site is called
3589/// open. An enum or an unknown name written this way is a type error the
3590/// checker reports, and reporting it here as well would say the wrong thing
3591/// about it.
3592fn calls_a_value(program: &Program, resolved: &ResolvedModule, name: &str) -> bool {
3593    let names_a_type = |owner: &ResolvedModule| {
3594        owner.structs.contains_key(name)
3595            || owner.enums.contains_key(name)
3596            || owner.aliases.contains_key(name)
3597    };
3598    !(declaring_module(program, resolved, name, names_a_type).is_some()
3599        || resolved.host_items.contains_key(name)
3600        || cove_schema::builtins::builtin(name).is_some()
3601        || cove_schema::builtins::free_builtin(name).is_some()
3602        || name == cove_schema::builtins::NONE_CASE.name)
3603}
3604
3605/// Records an edge whose callee the call site named, replacing an
3606/// approximate edge to the same callee: one call site naming it is enough to
3607/// make the edge real, whatever another site in the same body guessed.
3608fn exact(targets: &mut BTreeMap<Node, CallPrecision>, node: Node) {
3609    targets.insert(node, CallPrecision::Exact);
3610}
3611
3612/// The declarations `type_module.type_name`'s method `method` may reach.
3613///
3614/// [`Program::methods_of`] knows where a type's methods live, including the
3615/// ones a conformance declared in another module supplies, so this is a
3616/// filter over it rather than a second search.
3617fn type_methods(
3618    program: &Program,
3619    type_module: &str,
3620    type_name: &str,
3621    method: &str,
3622) -> BTreeSet<Node> {
3623    program
3624        .methods_of(type_module, type_name)
3625        .into_iter()
3626        .filter(|declared| declared.name == method)
3627        .map(|declared| {
3628            (
3629                declared.module.to_string(),
3630                FnKey::Method(type_name.to_string(), method.to_string()),
3631            )
3632        })
3633        .collect()
3634}
3635
3636/// The module that declares `name` as `resolved` sees it, when the
3637/// declaration it finds there satisfies `is_kind`.
3638fn declaring_module(
3639    program: &Program,
3640    resolved: &ResolvedModule,
3641    name: &str,
3642    is_kind: impl Fn(&ResolvedModule) -> bool,
3643) -> Option<String> {
3644    if is_kind(resolved) {
3645        return Some(resolved.name.clone());
3646    }
3647    let owner_name = resolved.imports.get(name)?;
3648    let owner = program.modules.get(owner_name)?;
3649    is_kind(owner).then(|| owner_name.clone())
3650}
3651
3652/// Fills in `required_capabilities` on every function and method of the
3653/// package as the least fixed point of "start from what a declaration calls
3654/// directly, then union in whatever every declaration it (transitively)
3655/// calls requires."
3656///
3657/// The graph is the package's, not one module's: a function that reaches
3658/// `console.println` only through an imported helper requires `console`.
3659///
3660/// The same round carries [`FnEntry::open_calls`] outward: a declaration
3661/// that calls a capability-open one is capability-open too, since the
3662/// requirement its callee could not see is one it cannot see either. Both
3663/// facts have to travel together, or a report could show a complete-looking
3664/// set that was assembled out of an incomplete one.
3665///
3666/// A fixed point rather than a recursive walk is required because the call
3667/// graph can be cyclic: direct and mutual recursion must not recurse forever.
3668/// Module imports may not form a cycle, but calls within a module still may.
3669/// Each round only ever adds to two finite sets, so the loop is guaranteed to
3670/// terminate.
3671fn propagate_capabilities(
3672    program: &mut Program,
3673    call_graph: &BTreeMap<Node, BTreeMap<Node, CallPrecision>>,
3674) {
3675    let mut required: BTreeMap<Node, BTreeSet<Capability>> = BTreeMap::new();
3676    let mut open: BTreeMap<Node, BTreeSet<OpenCall>> = BTreeMap::new();
3677    for (module, resolved) in &program.modules {
3678        for (name, entry) in &resolved.functions {
3679            let node = (module.clone(), FnKey::Fn(name.clone()));
3680            required.insert(node.clone(), entry.direct_capabilities.clone());
3681            open.insert(node, entry.direct_open_calls.clone());
3682        }
3683        for ((type_name, method_name), entry) in &resolved.methods {
3684            let node = (
3685                module.clone(),
3686                FnKey::Method(type_name.clone(), method_name.clone()),
3687            );
3688            required.insert(node.clone(), entry.direct_capabilities.clone());
3689            open.insert(node, entry.direct_open_calls.clone());
3690        }
3691    }
3692
3693    let keys: Vec<Node> = required.keys().cloned().collect();
3694    loop {
3695        let mut changed = false;
3696        for key in &keys {
3697            let Some(callees) = call_graph.get(key) else {
3698                continue;
3699            };
3700            let mut additions = BTreeSet::new();
3701            // One reason is enough for a caller: it says the set below it is
3702            // a floor, and the declaration that could not be followed says
3703            // which form it was.
3704            let mut reached_open = false;
3705            for callee in callees.keys() {
3706                if let Some(callee_open) = open.get(callee) {
3707                    reached_open |= !callee_open.is_empty();
3708                }
3709                let Some(callee_caps) = required.get(callee) else {
3710                    continue;
3711                };
3712                for cap in callee_caps {
3713                    if !required[key].contains(cap) {
3714                        additions.insert(cap.clone());
3715                    }
3716                }
3717            }
3718            if !additions.is_empty() {
3719                required.get_mut(key).unwrap().extend(additions);
3720                changed = true;
3721            }
3722            if reached_open && open.get_mut(key).unwrap().insert(OpenCall::ReachedOpenCall) {
3723                changed = true;
3724            }
3725        }
3726        if !changed {
3727            break;
3728        }
3729    }
3730
3731    for (module, resolved) in program.modules.iter_mut() {
3732        for (name, entry) in resolved.functions.iter_mut() {
3733            let node = (module.clone(), FnKey::Fn(name.clone()));
3734            entry.required_capabilities = required.remove(&node).unwrap_or_default();
3735            entry.open_calls = open.remove(&node).unwrap_or_default();
3736        }
3737        for ((type_name, method_name), entry) in resolved.methods.iter_mut() {
3738            let node = (
3739                module.clone(),
3740                FnKey::Method(type_name.clone(), method_name.clone()),
3741            );
3742            entry.required_capabilities = required.remove(&node).unwrap_or_default();
3743            entry.open_calls = open.remove(&node).unwrap_or_default();
3744        }
3745    }
3746}
3747
3748/// If `callee` is a call to a host module (`console.println(...)`) or an
3749/// unqualified host item (`println(...)`), the capability it requires.
3750///
3751/// The capability is the one the *operation's* schema declares, not the
3752/// module's, because that is the rule `HostRegistry::call_with` and
3753/// `call_resource` enforce at the boundary: they read
3754/// `OperationSchema::capability` and fall back to the module's only when the
3755/// operation itself is not in the schema. Deriving the module's capability
3756/// here instead would let the checker under-report what a call costs — an
3757/// embedder may gate one operation of a module more tightly than the module
3758/// as a whole, such as a `company` module whose `directory` operations need
3759/// only `directory` while `payroll` needs `payroll` — and a program that
3760/// requested only what the checker asked for would then be refused at run
3761/// time. A module no schema describes falls back to its name, which is the
3762/// only thing about it this compilation knows.
3763fn call_capability(
3764    callee: &Expr,
3765    host_uses: &BTreeSet<String>,
3766    host_items: &BTreeMap<String, String>,
3767    schemas: &HostSchemas,
3768) -> Option<Capability> {
3769    let (module, operation) = match &callee.kind {
3770        ExprKind::Field { base, name } => match &base.kind {
3771            ExprKind::Ident(module_name) if host_uses.contains(module_name.as_str()) => {
3772                (module_name.clone(), name.node.clone())
3773            }
3774            _ => return None,
3775        },
3776        ExprKind::Ident(name) => (host_items.get(name)?.clone(), name.clone()),
3777        _ => return None,
3778    };
3779    Some(operation_capability(&module, &operation, schemas))
3780}
3781
3782/// The capability a call to `module`'s `operation` requires, matching the
3783/// rule the Host API boundary enforces: the operation's own capability when
3784/// its schema declares one, the module's otherwise, and the module's name
3785/// when no schema describes the module at all.
3786fn operation_capability(module: &str, operation: &str, schemas: &HostSchemas) -> Capability {
3787    match schemas.module(module) {
3788        Some(schema) => match schema.operation(operation) {
3789            Some(op) => Capability::new(op.capability),
3790            None => Capability::new(schema.capability),
3791        },
3792        None => Capability::new(module),
3793    }
3794}
3795
3796#[cfg(test)]
3797mod tests {
3798    use super::*;
3799    use crate::config::Config;
3800    use crate::package::{Module, Unit};
3801    use cove_diag::SourceMap;
3802    use cove_schema::{Effect, HostType, ModuleSchema, OperationSchema};
3803    use std::path::PathBuf;
3804
3805    /// Builds a single module out of inline source texts, one per unit,
3806    /// without touching the filesystem.
3807    fn module_from_sources(name: &str, sources_text: &[&str]) -> Module {
3808        let mut sources = SourceMap::new();
3809        let mut units = Vec::new();
3810        for (i, text) in sources_text.iter().enumerate() {
3811            let path = PathBuf::from(format!("{name}{i}.cove"));
3812            let file = sources.add(path.clone(), *text);
3813            let ast = cove_syntax::parse_file(&sources, file).expect("test source parses");
3814            units.push(Unit { file, path, ast });
3815        }
3816        Module {
3817            name: name.to_string(),
3818            dir: PathBuf::from(name),
3819            units,
3820        }
3821    }
3822
3823    fn package_of(module: Module) -> Package {
3824        package_of_modules(vec![module])
3825    }
3826
3827    /// Builds a package out of several inline modules, so a `use` in one can
3828    /// be answered by another.
3829    fn package_of_modules(modules: Vec<Module>) -> Package {
3830        let mut map = BTreeMap::new();
3831        for module in modules {
3832            map.insert(module.name.clone(), module);
3833        }
3834        Package {
3835            root: PathBuf::new(),
3836            config: Config::default(),
3837            modules: map,
3838        }
3839    }
3840
3841    /// Resolves a package of inline modules, one source per module.
3842    fn resolve_modules(modules: &[(&str, &str)]) -> Result<Program, Vec<Diagnostic>> {
3843        let package = package_of_modules(
3844            modules
3845                .iter()
3846                .map(|(name, source)| module_from_sources(name, &[source]))
3847                .collect(),
3848        );
3849        resolve(&package)
3850    }
3851
3852    #[track_caller]
3853    fn resolve_ok(modules: &[(&str, &str)]) -> Program {
3854        match resolve_modules(modules) {
3855            Ok(program) => program,
3856            Err(errors) => panic!(
3857                "expected the package to resolve, found: {}",
3858                errors
3859                    .iter()
3860                    .map(|d| format!("{}: {}", d.code, d.message))
3861                    .collect::<Vec<_>>()
3862                    .join("; ")
3863            ),
3864        }
3865    }
3866
3867    /// Resolves a package expected to fail, and returns the one diagnostic
3868    /// with `code`.
3869    #[track_caller]
3870    fn resolve_err(modules: &[(&str, &str)], code: &str) -> Diagnostic {
3871        let errors = resolve_modules(modules).expect_err("expected the package to be rejected");
3872        errors
3873            .into_iter()
3874            .find(|d| d.code == code)
3875            .unwrap_or_else(|| panic!("expected a `{code}` diagnostic"))
3876    }
3877
3878    /// Resolves a package of inline modules against a Host API schema an
3879    /// embedder supplied, rather than the shipped set alone.
3880    fn resolve_modules_with(
3881        modules: &[(&str, &str)],
3882        schemas: &HostSchemas,
3883    ) -> Result<Program, Vec<Diagnostic>> {
3884        let package = package_of_modules(
3885            modules
3886                .iter()
3887                .map(|(name, source)| module_from_sources(name, &[source]))
3888                .collect(),
3889        );
3890        resolve_with(&package, schemas)
3891    }
3892
3893    #[track_caller]
3894    fn resolve_ok_with(modules: &[(&str, &str)], schemas: &HostSchemas) -> Program {
3895        match resolve_modules_with(modules, schemas) {
3896            Ok(program) => program,
3897            Err(errors) => panic!(
3898                "expected the package to resolve, found: {}",
3899                errors
3900                    .iter()
3901                    .map(|d| format!("{}: {}", d.code, d.message))
3902                    .collect::<Vec<_>>()
3903                    .join("; ")
3904            ),
3905        }
3906    }
3907
3908    #[test]
3909    fn records_a_test_and_leaves_it_module_private() {
3910        let program = resolve_ok(&[(
3911            "text",
3912            "fn wordCount(text: String) -> Int {\n  text.words().length()\n}\n\n             test fn countsWords() -> Result<Unit, Error> {\n  Ok(())\n}\n",
3913        )]);
3914        let entry = &program.modules["text"].functions["countsWords"];
3915        assert!(entry.is_test);
3916        assert!(!entry.exported);
3917        assert!(!program.modules["text"]
3918            .exports()
3919            .contains(&"countsWords".to_string()));
3920
3921        let tests = program.tests();
3922        assert_eq!(tests.len(), 1);
3923        assert_eq!(tests[0].qualified_name(), "text.countsWords");
3924    }
3925
3926    #[test]
3927    fn lists_every_test_of_the_package_in_module_then_name_order() {
3928        let program = resolve_ok(&[
3929            (
3930                "second",
3931                "test fn b() -> Result<Unit, Error> {\n  Ok(())\n}\n\n                 test fn a() -> Result<Unit, Error> {\n  Ok(())\n}\n",
3932            ),
3933            (
3934                "first",
3935                "test fn c() -> Result<Unit, Error> {\n  Ok(())\n}\n",
3936            ),
3937        ]);
3938        let names: Vec<String> = program
3939            .tests()
3940            .iter()
3941            .map(DeclaredTest::qualified_name)
3942            .collect();
3943        assert_eq!(names, ["first.c", "second.a", "second.b"]);
3944    }
3945
3946    #[test]
3947    fn a_test_requires_the_capabilities_its_call_graph_reaches() {
3948        let program = resolve_ok(&[(
3949            "text",
3950            "use console.println\n\n             fn report(text: String) -> Result<Unit, Error> {\n  println(text)\n}\n\n             test fn reports() -> Result<Unit, Error> {\n  report(\"a\")?\n  Ok(())\n}\n\n             test fn countsNothing() -> Result<Unit, Error> {\n  Ok(())\n}\n",
3951        )]);
3952        let required = |name: &str| -> Vec<String> {
3953            program.modules["text"].functions[name]
3954                .required_capabilities
3955                .iter()
3956                .map(Capability::to_string)
3957                .collect()
3958        };
3959        // Derived from the call graph exactly as any other function's are:
3960        // the test names no host module itself.
3961        assert_eq!(required("reports"), ["console".to_string()]);
3962        assert!(required("countsNothing").is_empty());
3963    }
3964
3965    #[test]
3966    fn a_test_may_call_its_modules_private_declarations() {
3967        let program = resolve_ok(&[(
3968            "text",
3969            "fn secret() -> Int {\n  7\n}\n\n             test fn seesSecret() -> Result<Unit, Error> {\n  secret()\n  Ok(())\n}\n",
3970        )]);
3971        let edges = &program.call_graph[&("text".to_string(), FnKey::Fn("seesSecret".to_string()))];
3972        assert!(edges.contains_key(&("text".to_string(), FnKey::Fn("secret".to_string()))));
3973    }
3974
3975    #[test]
3976    fn merges_two_units_of_the_same_module() {
3977        let module = module_from_sources(
3978            "greet",
3979            &[
3980                "/// Greets by name.\nexport fn greet(name: String) -> String {\n  name\n}\n",
3981                "/// Says goodbye.\nexport fn farewell(name: String) -> String {\n  name\n}\n",
3982            ],
3983        );
3984        let package = package_of(module);
3985        let program = resolve(&package).expect("resolves");
3986        let resolved = &program.modules["greet"];
3987        assert!(resolved.functions.contains_key("greet"));
3988        assert!(resolved.functions.contains_key("farewell"));
3989    }
3990
3991    #[test]
3992    fn reports_duplicate_declaration_across_units() {
3993        let module = module_from_sources(
3994            "dup",
3995            &[
3996                "/// First.\nexport fn greet(name: String) -> String {\n  name\n}\n",
3997                "/// Second.\nexport fn greet(name: String) -> String {\n  name\n}\n",
3998            ],
3999        );
4000        let package = package_of(module);
4001        let errs = resolve(&package).unwrap_err();
4002        assert!(errs
4003            .iter()
4004            .any(|d| d.code == "cove::resolve::duplicate_declaration"));
4005    }
4006
4007    #[test]
4008    fn resolves_impl_methods() {
4009        let module = module_from_sources(
4010            "booking",
4011            &[
4012                "/// A booking.\nexport struct Booking {\n  id: String\n}\n\nimpl Booking {\n  /// Returns the id.\n  fn id(self) -> String {\n    self.id\n  }\n}\n",
4013            ],
4014        );
4015        let package = package_of(module);
4016        let program = resolve(&package).expect("resolves");
4017        let resolved = &program.modules["booking"];
4018        let method = resolved
4019            .methods
4020            .get(&("Booking".to_string(), "id".to_string()))
4021            .expect("method resolved");
4022        assert_eq!(method.receiver_type.as_deref(), Some("Booking"));
4023    }
4024
4025    /// The trait and the two types every conformance test below builds on.
4026    const TRAIT_SOURCE: &str = "\
4027/// Renders itself.
4028export trait Display {
4029  /// The full form.
4030  fn describe(self) -> String
4031
4032  /// A short form, defaulting to the full one.
4033  fn label(self) -> String { self.describe() }
4034}
4035
4036/// A booking.
4037export struct Booking(id: Int)
4038
4039/// A receipt.
4040export struct Receipt(total: Int)
4041";
4042
4043    fn resolved_of(name: &str, sources: &[&str]) -> ResolvedModule {
4044        let package = package_of(module_from_sources(name, sources));
4045        let mut program = resolve(&package).expect("resolves");
4046        program.modules.remove(name).expect("the module resolves")
4047    }
4048
4049    fn resolve_errors(name: &str, sources: &[&str]) -> Vec<Diagnostic> {
4050        let package = package_of(module_from_sources(name, sources));
4051        resolve(&package).expect_err("expected resolution to fail")
4052    }
4053
4054    fn has_code(diagnostics: &[Diagnostic], code: &str) -> bool {
4055        diagnostics.iter().any(|d| d.code == code)
4056    }
4057
4058    #[test]
4059    fn records_a_conformance_and_the_methods_it_supplies() {
4060        let source = format!(
4061            "{TRAIT_SOURCE}\nimpl Display for Booking {{\n  fn describe(self) -> String {{ \"b\" }}\n  fn label(self) -> String {{ \"#\" }}\n}}\n"
4062        );
4063        let resolved = resolved_of("render", &[&source]);
4064        let conformance = resolved
4065            .conformances
4066            .get(&("Display".to_string(), "Booking".to_string()))
4067            .expect("the conformance is recorded");
4068        assert_eq!(
4069            conformance.methods.iter().cloned().collect::<Vec<_>>(),
4070            ["describe", "label"]
4071        );
4072        // A conformance's methods are ordinary methods of the type, which is
4073        // what lets dispatch find them without asking where they came from.
4074        assert!(resolved
4075            .methods
4076            .contains_key(&("Booking".to_string(), "describe".to_string())));
4077    }
4078
4079    #[test]
4080    fn a_defaulted_method_becomes_the_type_s_own_method() {
4081        let source = format!(
4082            "{TRAIT_SOURCE}\nimpl Display for Receipt {{\n  fn describe(self) -> String {{ \"r\" }}\n}}\n"
4083        );
4084        let resolved = resolved_of("render", &[&source]);
4085        let label = resolved
4086            .methods
4087            .get(&("Receipt".to_string(), "label".to_string()))
4088            .expect("the default body is recorded as a method");
4089        assert_eq!(label.receiver_type.as_deref(), Some("Receipt"));
4090        assert_eq!(
4091            label.doc.as_deref(),
4092            Some("A short form, defaulting to the full one.")
4093        );
4094    }
4095
4096    #[test]
4097    fn rejects_a_conformance_missing_a_required_method() {
4098        let source = format!("{TRAIT_SOURCE}\nimpl Display for Booking {{\n}}\n");
4099        let errors = resolve_errors("render", &[&source]);
4100        assert!(has_code(&errors, "cove::resolve::missing_trait_method"));
4101        assert!(errors[0].message.contains("`describe`"));
4102        // `label` has a default, so it is not missing.
4103        assert!(!errors[0].message.contains("`label`"));
4104    }
4105
4106    #[test]
4107    fn rejects_a_method_the_trait_does_not_declare() {
4108        let source = format!(
4109            "{TRAIT_SOURCE}\nimpl Display for Booking {{\n  fn describe(self) -> String {{ \"b\" }}\n  fn extra(self) -> Int {{ 1 }}\n}}\n"
4110        );
4111        let errors = resolve_errors("render", &[&source]);
4112        assert!(has_code(&errors, "cove::resolve::unknown_trait_method"));
4113    }
4114
4115    #[test]
4116    fn rejects_the_same_conformance_twice() {
4117        let source = format!(
4118            "{TRAIT_SOURCE}\nimpl Display for Booking {{\n  fn describe(self) -> String {{ \"b\" }}\n}}\n\nimpl Display for Booking {{\n  fn describe(self) -> String {{ \"c\" }}\n}}\n"
4119        );
4120        let errors = resolve_errors("render", &[&source]);
4121        assert!(has_code(&errors, "cove::resolve::duplicate_conformance"));
4122    }
4123
4124    #[test]
4125    fn rejects_a_trait_method_that_collides_with_an_inherent_method() {
4126        let source = format!(
4127            "{TRAIT_SOURCE}\nimpl Display for Booking {{\n  fn describe(self) -> String {{ \"b\" }}\n}}\n\nimpl Booking {{\n  /// Also describes.\n  fn describe(self) -> String {{ \"c\" }}\n}}\n"
4128        );
4129        let errors = resolve_errors("render", &[&source]);
4130        assert!(has_code(&errors, "cove::resolve::duplicate_declaration"));
4131    }
4132
4133    #[test]
4134    fn the_orphan_rule_allows_a_local_trait_or_a_local_type() {
4135        // The trait is local, the type is not declared here at all: the
4136        // orphan rule is satisfied, and what fails is the separate rule that
4137        // an `impl` extends a struct or enum of this module.
4138        let source = format!("{TRAIT_SOURCE}\nimpl Display for Int {{\n  fn describe(self) -> String {{ \"i\" }}\n}}\n");
4139        let errors = resolve_errors("render", &[&source]);
4140        assert!(has_code(&errors, "cove::resolve::unknown_impl_type"));
4141        assert!(!has_code(&errors, "cove::resolve::orphan_conformance"));
4142    }
4143
4144    #[test]
4145    fn rejects_a_conformance_between_two_types_the_module_does_not_declare() {
4146        let errors = resolve_errors(
4147            "elsewhere",
4148            &["impl Display for Int {\n  fn describe(self) -> String { \"i\" }\n}\n"],
4149        );
4150        assert!(has_code(&errors, "cove::resolve::orphan_conformance"));
4151    }
4152
4153    // ------------------------------------------------- the builtin `Snapshot`
4154
4155    #[test]
4156    fn impl_snapshot_records_a_conformance_with_no_trait_declaration_in_source() {
4157        let source = "\
4158/// A booking.
4159export struct Booking(id: Int)
4160
4161impl Snapshot for Booking {
4162  /// Returns a copy of this booking.
4163  fn snapshot(self) -> Booking { self }
4164}
4165";
4166        let resolved = resolved_of("booking", &[source]);
4167        let conformance = resolved
4168            .conformances
4169            .get(&("Snapshot".to_string(), "Booking".to_string()))
4170            .expect("the conformance is recorded even though no `trait Snapshot` was written");
4171        assert_eq!(
4172            conformance.methods.iter().cloned().collect::<Vec<_>>(),
4173            ["snapshot"]
4174        );
4175        assert!(resolved
4176            .methods
4177            .contains_key(&("Booking".to_string(), "snapshot".to_string())));
4178        // `Snapshot` itself is not a declaration this module makes: it
4179        // belongs to no module.
4180        assert!(!resolved.traits.contains_key("Snapshot"));
4181    }
4182
4183    #[test]
4184    fn impl_snapshot_still_requires_the_snapshot_method() {
4185        let source = "\
4186/// A booking.
4187export struct Booking(id: Int)
4188
4189impl Snapshot for Booking {
4190}
4191";
4192        let errors = resolve_errors("booking", &[source]);
4193        assert!(has_code(&errors, "cove::resolve::missing_trait_method"));
4194        assert!(errors[0].message.contains("`snapshot`"));
4195    }
4196
4197    #[test]
4198    fn a_third_module_may_not_conform_an_imported_type_to_snapshot() {
4199        // `Snapshot` belongs to no module, so the orphan rule's only way to
4200        // pass is the type's own module declaring it; a module that merely
4201        // imports `Booking` cannot conform it.
4202        let error = resolve_err(
4203            &[
4204                (
4205                    "booking",
4206                    "/// A booking.\nexport struct Booking(id: Int)\n",
4207                ),
4208                (
4209                    "other",
4210                    "use booking.Booking\n\nimpl Snapshot for Booking {\n  fn snapshot(self) -> Booking { self }\n}\n",
4211                ),
4212            ],
4213            "cove::resolve::orphan_conformance",
4214        );
4215        assert!(error.message.contains("Snapshot"));
4216        assert!(error.message.contains("Booking"));
4217    }
4218
4219    #[test]
4220    fn warns_on_an_exported_trait_and_its_methods_without_docs() {
4221        let package = package_of(module_from_sources(
4222            "render",
4223            &["export trait Display {\n  fn describe(self) -> String\n}\n"],
4224        ));
4225        let program = resolve(&package).expect("resolves");
4226        let names: Vec<&str> = program
4227            .notices
4228            .iter()
4229            .filter(|d| d.code == "cove::resolve::missing_doc")
4230            .map(|d| d.message.as_str())
4231            .collect();
4232        assert_eq!(
4233            names,
4234            [
4235                "exported `Display` has no doc comment",
4236                "exported `Display.describe` has no doc comment"
4237            ]
4238        );
4239    }
4240
4241    #[test]
4242    fn a_defaulted_method_is_marked_as_coming_from_its_trait() {
4243        let source = format!(
4244            "{TRAIT_SOURCE}\nimpl Display for Receipt {{\n  fn describe(self) -> String {{ \"r\" }}\n}}\n"
4245        );
4246        let resolved = resolved_of("render", &[&source]);
4247        let methods = &resolved.methods;
4248        assert_eq!(
4249            methods[&("Receipt".to_string(), "label".to_string())]
4250                .from_trait_default
4251                .as_deref(),
4252            Some("Display")
4253        );
4254        assert!(methods[&("Receipt".to_string(), "describe".to_string())]
4255            .from_trait_default
4256            .is_none());
4257    }
4258
4259    #[test]
4260    fn a_default_body_s_match_is_checked_once_however_many_types_conform() {
4261        let source = "\
4262/// A signal.
4263enum Signal {
4264  Red
4265  Green
4266}
4267
4268/// Shows itself.
4269trait Show {
4270  /// The signal.
4271  fn signal(self) -> Signal
4272
4273  /// A name, from a `match` that misses a case.
4274  fn name(self) -> String {
4275    match self.signal() {
4276      Signal.Red => \"red\"
4277    }
4278  }
4279}
4280
4281/// One.
4282struct A(x: Int)
4283
4284/// Two.
4285struct B(x: Int)
4286
4287impl Show for A {
4288  fn signal(self) -> Signal { Signal.Red }
4289}
4290
4291impl Show for B {
4292  fn signal(self) -> Signal { Signal.Green }
4293}
4294";
4295        let errors = resolve_errors("show", &[source]);
4296        assert_eq!(
4297            errors
4298                .iter()
4299                .filter(|d| d.code == "cove::resolve::non_exhaustive_match")
4300                .count(),
4301            1
4302        );
4303    }
4304
4305    #[test]
4306    fn a_conformance_method_propagates_its_capabilities() {
4307        let source = format!(
4308            "use console.println\n\n{TRAIT_SOURCE}\nimpl Display for Booking {{\n  fn describe(self) -> String {{\n    console.println(\"b\")\n    \"b\"\n  }}\n}}\n"
4309        );
4310        let resolved = resolved_of("render", &[&source]);
4311        let describe = &resolved.methods[&("Booking".to_string(), "describe".to_string())];
4312        assert!(describe
4313            .required_capabilities
4314            .iter()
4315            .any(|c| c.to_string() == "console"));
4316    }
4317
4318    #[test]
4319    fn rejects_impl_for_unknown_type() {
4320        let module = module_from_sources("orphan", &["impl Nothing {\n  fn go(self) {\n  }\n}\n"]);
4321        let package = package_of(module);
4322        let errs = resolve(&package).unwrap_err();
4323        assert!(errs
4324            .iter()
4325            .any(|d| d.code == "cove::resolve::unknown_impl_type"));
4326    }
4327
4328    #[test]
4329    fn rejects_non_fn_impl_items() {
4330        let module = module_from_sources(
4331            "badimpl",
4332            &[
4333                "export struct Thing {\n  x: Int\n}\n\nimpl Thing {\n  struct Nested {\n    y: Int\n  }\n}\n",
4334            ],
4335        );
4336        let package = package_of(module);
4337        let errs = resolve(&package).unwrap_err();
4338        assert!(errs
4339            .iter()
4340            .any(|d| d.code == "cove::resolve::invalid_impl_item"));
4341    }
4342
4343    #[test]
4344    fn one_segment_use_records_a_host_use() {
4345        let module = module_from_sources("hostuse", &["use http\n\nexport fn main() {\n}\n"]);
4346        let package = package_of(module);
4347        let program = resolve(&package).expect("resolves");
4348        assert!(program.modules["hostuse"].host_uses.contains("http"));
4349    }
4350
4351    #[test]
4352    fn two_segment_use_records_use_and_item() {
4353        let module = module_from_sources(
4354            "hostitem",
4355            &["use console.println\n\nexport fn main() {\n}\n"],
4356        );
4357        let package = package_of(module);
4358        let program = resolve(&package).expect("resolves");
4359        let resolved = &program.modules["hostitem"];
4360        assert!(resolved.host_uses.contains("console"));
4361        assert_eq!(
4362            resolved.host_items.get("println").map(String::as_str),
4363            Some("console")
4364        );
4365    }
4366
4367    #[test]
4368    fn a_use_matching_no_module_and_no_host_path_is_rejected() {
4369        let diagnostic = resolve_err(&[("toolong", "use a.b.c\n")], "cove::resolve::unknown_use");
4370        assert!(diagnostic.message.contains("a.b.c"));
4371        // The message names both things the compiler looked for.
4372        assert!(diagnostic.message.contains("module"));
4373        assert!(diagnostic.message.contains("host module"));
4374    }
4375
4376    // ------------------------------------------------------- module imports
4377
4378    #[test]
4379    fn a_use_imports_an_exported_declaration() {
4380        let program = resolve_ok(&[
4381            (
4382                "greet",
4383                "/// Greets by name.\nexport fn greeting(name: String) -> String {\n  name\n}\n",
4384            ),
4385            (
4386                "hello",
4387                "use greet.greeting\n\n/// Entry point.\nexport fn main() -> String {\n  greeting(\"world\")\n}\n",
4388            ),
4389        ]);
4390        assert_eq!(
4391            program.modules["hello"].imports.get("greeting"),
4392            Some(&"greet".to_string())
4393        );
4394        assert!(program.modules["hello"].module_imports.is_empty());
4395        // A module of this package is not a host module, so nothing was
4396        // recorded as one.
4397        assert!(program.modules["hello"].host_uses.is_empty());
4398    }
4399
4400    #[test]
4401    fn a_use_of_a_module_alone_imports_the_module() {
4402        let program = resolve_ok(&[
4403            (
4404                "greet",
4405                "/// Greets by name.\nexport fn greeting(name: String) -> String {\n  name\n}\n",
4406            ),
4407            (
4408                "hello",
4409                "use greet\n\n/// Entry point.\nexport fn main() -> String {\n  greet.greeting(\"world\")\n}\n",
4410            ),
4411        ]);
4412        assert_eq!(
4413            program.modules["hello"].module_imports.get("greet"),
4414            Some(&"greet".to_string())
4415        );
4416        assert!(program.modules["hello"].imports.is_empty());
4417        assert!(program.modules["hello"].host_uses.is_empty());
4418    }
4419
4420    #[test]
4421    fn a_nested_module_is_imported_by_its_full_path() {
4422        let program = resolve_ok(&[
4423            (
4424                "src.booking",
4425                "/// Creates a booking.\nexport fn createBooking() -> String {\n  \"b\"\n}\n",
4426            ),
4427            (
4428                "app",
4429                "use src.booking.createBooking\n\n/// Entry point.\nexport fn main() -> String {\n  createBooking()\n}\n",
4430            ),
4431        ]);
4432        assert_eq!(
4433            program.modules["app"].imports.get("createBooking"),
4434            Some(&"src.booking".to_string())
4435        );
4436    }
4437
4438    #[test]
4439    fn a_use_of_a_private_declaration_is_rejected() {
4440        let diagnostic = resolve_err(
4441            &[
4442                (
4443                    "greet",
4444                    "fn greeting(name: String) -> String {\n  name\n}\n",
4445                ),
4446                ("hello", "use greet.greeting\n"),
4447            ],
4448            "cove::resolve::private_declaration",
4449        );
4450        assert!(diagnostic.message.contains("not exported"));
4451        // The declaration itself is labelled, and `export` is the fix.
4452        assert_eq!(diagnostic.labels.len(), 1);
4453        assert!(diagnostic.help.as_deref().unwrap().contains("export"));
4454    }
4455
4456    #[test]
4457    fn a_use_naming_a_module_that_declares_no_such_name_is_rejected() {
4458        let diagnostic = resolve_err(
4459            &[
4460                (
4461                    "greet",
4462                    "/// Greets.\nexport fn greeting() -> String {\n  \"hi\"\n}\n",
4463                ),
4464                ("hello", "use greet.farewell\n"),
4465            ],
4466            "cove::resolve::unknown_use",
4467        );
4468        assert!(diagnostic.message.contains("declares no `farewell`"));
4469        assert!(diagnostic.message.contains("not a host module"));
4470        assert!(diagnostic.help.as_deref().unwrap().contains("greeting"));
4471    }
4472
4473    #[test]
4474    fn a_module_named_after_a_host_module_is_rejected_rather_than_preferred() {
4475        let diagnostic = resolve_err(
4476            &[
4477                (
4478                    "console",
4479                    "/// Prints.\nexport fn println(line: String) {\n}\n",
4480                ),
4481                ("app", "use console.println\n"),
4482            ],
4483            "cove::resolve::module_shadows_host",
4484        );
4485        assert!(diagnostic.help.as_deref().unwrap().contains("rename"));
4486    }
4487
4488    /// Every host module the shipped schema describes is refused as a
4489    /// package module, or a package module of that name shadows it silently
4490    /// -- modules resolve first.
4491    ///
4492    /// The loop reads [`host_modules`] rather than repeating it, because a
4493    /// second copy of the list is a second place for a host to go missing:
4494    /// `http` was absent from both for as long as this test spelled its own
4495    /// names out.
4496    #[test]
4497    fn every_shipped_host_module_is_refused_as_a_package_module() {
4498        for host in host_modules(&HostSchemas::new()) {
4499            let diagnostic = resolve_err(
4500                &[
4501                    (host, "/// Does something.\nexport fn thing() {\n}\n"),
4502                    ("app", &format!("use {host}.thing\n")),
4503                ],
4504                "cove::resolve::module_shadows_host",
4505            );
4506            assert!(
4507                diagnostic.message.contains(host),
4508                "`{host}` should be refused as a package module"
4509            );
4510        }
4511    }
4512
4513    #[test]
4514    fn a_use_naming_both_a_module_and_a_declaration_is_rejected() {
4515        let diagnostic = resolve_err(
4516            &[
4517                (
4518                    "booking",
4519                    "/// Creates a booking.\nexport fn create() -> String {\n  \"b\"\n}\n",
4520                ),
4521                (
4522                    "booking.create",
4523                    "/// Validates a booking.\nexport fn validate() -> Bool {\n  true\n}\n",
4524                ),
4525                ("app", "use booking.create\n"),
4526            ],
4527            "cove::resolve::ambiguous_use",
4528        );
4529        assert!(diagnostic.message.contains("both"));
4530    }
4531
4532    #[test]
4533    fn an_import_colliding_with_a_declaration_is_rejected() {
4534        let diagnostic = resolve_err(
4535            &[
4536                (
4537                    "greet",
4538                    "/// Greets.\nexport fn greeting() -> String {\n  \"hi\"\n}\n",
4539                ),
4540                (
4541                    "hello",
4542                    "use greet.greeting\n\nfn greeting() -> String {\n  \"other\"\n}\n",
4543                ),
4544            ],
4545            "cove::resolve::import_conflict",
4546        );
4547        assert!(diagnostic.message.contains("also declares it"));
4548    }
4549
4550    #[test]
4551    fn two_imports_of_one_name_from_different_modules_are_rejected() {
4552        let diagnostic = resolve_err(
4553            &[
4554                (
4555                    "left",
4556                    "/// Greets.\nexport fn greeting() -> String {\n  \"l\"\n}\n",
4557                ),
4558                (
4559                    "right",
4560                    "/// Greets.\nexport fn greeting() -> String {\n  \"r\"\n}\n",
4561                ),
4562                ("hello", "use left.greeting\nuse right.greeting\n"),
4563            ],
4564            "cove::resolve::import_conflict",
4565        );
4566        assert!(diagnostic.message.contains("both"));
4567    }
4568
4569    #[test]
4570    fn importing_the_same_declaration_twice_is_not_a_conflict() {
4571        let package = package_of_modules(vec![
4572            module_from_sources(
4573                "greet",
4574                &["/// Greets.\nexport fn greeting() -> String {\n  \"hi\"\n}\n"],
4575            ),
4576            module_from_sources("hello", &["use greet.greeting\n", "use greet.greeting\n"]),
4577        ]);
4578        resolve(&package).expect("resolves");
4579    }
4580
4581    #[test]
4582    fn an_unknown_two_segment_use_is_still_a_host_path() {
4583        let program = resolve_ok(&[("app", "use other.println\n")]);
4584        assert!(program.modules["app"].host_uses.contains("other"));
4585        assert_eq!(
4586            program.modules["app"].host_items.get("println"),
4587            Some(&"other".to_string())
4588        );
4589    }
4590
4591    /// A package where the same undescribed host module is named by two
4592    /// `use`s in one module (unqualified, then qualified to an item) *and*
4593    /// by a `use` in a second module gets one `unchecked_host` warning, not
4594    /// three: repeating the warning per `use` would inflate a
4595    /// `cove check --deny-warnings` count for one unknown module.
4596    ///
4597    /// The single warning is pinned to the first `use` of the
4598    /// alphabetically first module that names the module, because
4599    /// `package.modules` is a `BTreeMap`: module `a` sorts before `b`, and
4600    /// within `a` its first unit's `use company` sorts before its second
4601    /// unit's `use company.employee`.
4602    #[test]
4603    fn unchecked_host_warns_once_per_module_not_once_per_use() {
4604        let module_a = module_from_sources(
4605            "a",
4606            &[
4607                "use company\n\n/// Calls into `company`.\nexport fn f() {\n  company.employee()\n}\n",
4608                "use company.employee\n\n/// Calls the unqualified import.\nexport fn g() {\n  employee()\n}\n",
4609            ],
4610        );
4611        let first_use_file = module_a.units[0].file;
4612        let module_b = module_from_sources(
4613            "b",
4614            &["use company\n\n/// Also calls into `company`.\nexport fn h() {\n  company.employee()\n}\n"],
4615        );
4616        let package = package_of_modules(vec![module_a, module_b]);
4617        let program = resolve(&package).expect("resolves despite the unchecked host warning");
4618        let warnings: Vec<_> = program
4619            .notices
4620            .iter()
4621            .filter(|d| d.code == "cove::resolve::unchecked_host")
4622            .collect();
4623        assert_eq!(
4624            warnings.len(),
4625            1,
4626            "expected exactly one unchecked_host warning, found {warnings:?}"
4627        );
4628        assert_eq!(
4629            warnings[0].primary.expect("warning has a span").file,
4630            first_use_file,
4631            "the warning should point at module `a`'s first `use company`"
4632        );
4633    }
4634
4635    #[test]
4636    fn unchecked_host_warns_once_per_distinct_module() {
4637        let program = resolve_ok(&[
4638            (
4639                "a",
4640                "use company\n\n/// Calls into `company`.\nexport fn f() {\n  company.employee()\n}\n",
4641            ),
4642            (
4643                "b",
4644                "use vendor\n\n/// Calls into `vendor`.\nexport fn g() {\n  vendor.order()\n}\n",
4645            ),
4646        ]);
4647        let modules_warned: BTreeSet<&str> = program
4648            .notices
4649            .iter()
4650            .filter(|d| d.code == "cove::resolve::unchecked_host")
4651            .map(|d| {
4652                if d.message.contains("`company`") {
4653                    "company"
4654                } else if d.message.contains("`vendor`") {
4655                    "vendor"
4656                } else {
4657                    panic!("unexpected unchecked_host warning: {}", d.message)
4658                }
4659            })
4660            .collect();
4661        assert_eq!(
4662            modules_warned,
4663            BTreeSet::from(["company", "vendor"]),
4664            "two distinct undescribed host modules should each warn once"
4665        );
4666    }
4667
4668    // -------------------------------------------------------------- cycles
4669
4670    #[test]
4671    fn a_direct_import_cycle_is_rejected() {
4672        let diagnostic = resolve_err(
4673            &[
4674                (
4675                    "a",
4676                    "use b.fromB\n\n/// Exported.\nexport fn fromA() -> Int {\n  1\n}\n",
4677                ),
4678                (
4679                    "b",
4680                    "use a.fromA\n\n/// Exported.\nexport fn fromB() -> Int {\n  2\n}\n",
4681                ),
4682            ],
4683            "cove::resolve::import_cycle",
4684        );
4685        assert!(
4686            diagnostic.message.contains("a -> b -> a")
4687                || diagnostic.message.contains("b -> a -> b")
4688        );
4689    }
4690
4691    #[test]
4692    fn a_transitive_import_cycle_is_rejected() {
4693        let diagnostic = resolve_err(
4694            &[
4695                (
4696                    "a",
4697                    "use b.fromB\n\n/// Exported.\nexport fn fromA() -> Int {\n  1\n}\n",
4698                ),
4699                (
4700                    "b",
4701                    "use c.fromC\n\n/// Exported.\nexport fn fromB() -> Int {\n  2\n}\n",
4702                ),
4703                (
4704                    "c",
4705                    "use a.fromA\n\n/// Exported.\nexport fn fromC() -> Int {\n  3\n}\n",
4706                ),
4707            ],
4708            "cove::resolve::import_cycle",
4709        );
4710        assert!(diagnostic.message.contains(" -> "));
4711        assert_eq!(
4712            resolve_modules(&[
4713                (
4714                    "a",
4715                    "use b.fromB\n\n/// Exported.\nexport fn fromA() -> Int {\n  1\n}\n",
4716                ),
4717                (
4718                    "b",
4719                    "use c.fromC\n\n/// Exported.\nexport fn fromB() -> Int {\n  2\n}\n",
4720                ),
4721                (
4722                    "c",
4723                    "use a.fromA\n\n/// Exported.\nexport fn fromC() -> Int {\n  3\n}\n",
4724                ),
4725            ])
4726            .unwrap_err()
4727            .iter()
4728            .filter(|d| d.code == "cove::resolve::import_cycle")
4729            .count(),
4730            1,
4731            "one cycle is reported once, however many modules it runs through"
4732        );
4733    }
4734
4735    #[test]
4736    fn a_module_importing_itself_is_a_cycle() {
4737        resolve_err(
4738            &[(
4739                "a",
4740                "use a.fromA\n\n/// Exported.\nexport fn fromA() -> Int {\n  1\n}\n",
4741            )],
4742            "cove::resolve::import_cycle",
4743        );
4744    }
4745
4746    /// A diamond is ordinary: importing a module runs none of its code, so
4747    /// two modules may import a third without any ordering question.
4748    #[test]
4749    fn a_diamond_import_is_accepted() {
4750        let program = resolve_ok(&[
4751            (
4752                "base",
4753                "/// The shared helper.\nexport fn base() -> Int {\n  1\n}\n",
4754            ),
4755            (
4756                "left",
4757                "use base.base\n\n/// Exported.\nexport fn left() -> Int {\n  base()\n}\n",
4758            ),
4759            (
4760                "right",
4761                "use base.base\n\n/// Exported.\nexport fn right() -> Int {\n  base()\n}\n",
4762            ),
4763            (
4764                "top",
4765                "use left.left\nuse right.right\n\n/// Exported.\nexport fn top() -> Int {\n  left() + right()\n}\n",
4766            ),
4767        ]);
4768        assert_eq!(program.modules.len(), 4);
4769    }
4770
4771    // ------------------------------------------- capabilities across modules
4772
4773    #[test]
4774    fn required_capabilities_cross_a_module_boundary() {
4775        let program = resolve_ok(&[
4776            (
4777                "log",
4778                "use console.println\n\n/// Logs a message.\nexport fn log(msg: String) {\n  console.println(msg)\n}\n",
4779            ),
4780            (
4781                "app",
4782                "use log.log\n\n/// Entry point; never names a host module.\nexport fn main() {\n  log(\"hi\")\n}\n",
4783            ),
4784        ]);
4785        let main = &program.modules["app"].functions["main"];
4786        assert!(main.direct_capabilities.is_empty());
4787        assert!(main
4788            .required_capabilities
4789            .contains(&Capability::new("console")));
4790    }
4791
4792    #[test]
4793    fn required_capabilities_cross_a_qualified_module_call() {
4794        let program = resolve_ok(&[
4795            (
4796                "log",
4797                "use console.println\n\n/// Logs a message.\nexport fn log(msg: String) {\n  console.println(msg)\n}\n",
4798            ),
4799            (
4800                "app",
4801                "use log\n\n/// Entry point.\nexport fn main() {\n  log.log(\"hi\")\n}\n",
4802            ),
4803        ]);
4804        assert!(program.modules["app"].functions["main"]
4805            .required_capabilities
4806            .contains(&Capability::new("console")));
4807    }
4808
4809    #[test]
4810    fn required_capabilities_cross_two_module_boundaries() {
4811        let program = resolve_ok(&[
4812            (
4813                "bottom",
4814                "use console.println\n\n/// Logs.\nexport fn log(msg: String) {\n  console.println(msg)\n}\n",
4815            ),
4816            (
4817                "middle",
4818                "use bottom.log\n\n/// Logs twice.\nexport fn twice(msg: String) {\n  log(msg)\n  log(msg)\n}\n",
4819            ),
4820            (
4821                "top",
4822                "use middle.twice\n\n/// Entry point.\nexport fn main() {\n  twice(\"hi\")\n}\n",
4823            ),
4824        ]);
4825        assert!(program.modules["top"].functions["main"]
4826            .required_capabilities
4827            .contains(&Capability::new("console")));
4828    }
4829
4830    #[test]
4831    fn required_capabilities_cross_an_imported_type_s_method() {
4832        let program = resolve_ok(&[
4833            (
4834                "thing",
4835                "use console.println\n\n/// A thing.\nexport struct Thing {\n  id: String\n}\n\n\
4836                 impl Thing {\n  /// Prints the id.\n  fn touch(self) {\n    console.println(self.id)\n  }\n}\n",
4837            ),
4838            (
4839                "app",
4840                "use thing.Thing\n\n/// Entry point.\nexport fn main() {\n  Thing.touch()\n}\n",
4841            ),
4842        ]);
4843        assert!(program.modules["app"].functions["main"]
4844            .required_capabilities
4845            .contains(&Capability::new("console")));
4846    }
4847
4848    /// A receiver whose type the resolver cannot know reaches every method
4849    /// of that name in this module *and* in the modules it imports, which is
4850    /// what keeps the over-approximation sound across a boundary.
4851    #[test]
4852    fn an_unknown_receiver_reaches_an_imported_type_s_method() {
4853        let program = resolve_ok(&[
4854            (
4855                "thing",
4856                "use console.println\n\n/// A thing.\nexport struct Thing {\n  id: String\n}\n\n\
4857                 impl Thing {\n  /// Prints the id.\n  fn touch(self) {\n    console.println(self.id)\n  }\n}\n",
4858            ),
4859            (
4860                "app",
4861                "use thing.Thing\n\n/// Entry point.\nexport fn main(value: Thing) {\n  value.touch()\n}\n",
4862            ),
4863        ]);
4864        assert!(program.modules["app"].functions["main"]
4865            .required_capabilities
4866            .contains(&Capability::new("console")));
4867    }
4868
4869    #[test]
4870    fn a_module_that_imports_nothing_requires_nothing_from_its_neighbours() {
4871        let program = resolve_ok(&[
4872            (
4873                "log",
4874                "use console.println\n\n/// Logs a message.\nexport fn log(msg: String) {\n  console.println(msg)\n}\n",
4875            ),
4876            (
4877                "pure",
4878                "/// Adds.\nexport fn add(a: Int, b: Int) -> Int {\n  a + b\n}\n",
4879            ),
4880        ]);
4881        assert!(program.modules["pure"].functions["add"]
4882            .required_capabilities
4883            .is_empty());
4884    }
4885
4886    // --------------------------------------------- imported enums in `match`
4887
4888    #[test]
4889    fn match_over_an_imported_enum_is_checked_for_exhaustiveness() {
4890        let diagnostic = resolve_err(
4891            &[
4892                (
4893                    "levels",
4894                    "/// Levels.\nexport enum LogLevel {\n  Debug\n  Info\n  Warn\n}\n",
4895                ),
4896                (
4897                    "app",
4898                    "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n  \
4899                     match level {\n    LogLevel.Debug => \"debug\"\n    LogLevel.Info => \"info\"\n  }\n}\n",
4900                ),
4901            ],
4902            "cove::resolve::non_exhaustive_match",
4903        );
4904        assert!(diagnostic.message.contains("LogLevel.Warn"));
4905    }
4906
4907    #[test]
4908    fn match_covering_every_case_of_an_imported_enum_passes() {
4909        let program = resolve_ok(&[
4910            (
4911                "levels",
4912                "/// Levels.\nexport enum LogLevel {\n  Debug\n  Info\n}\n",
4913            ),
4914            (
4915                "app",
4916                "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n  \
4917                 match level {\n    LogLevel.Debug => \"debug\"\n    LogLevel.Info => \"info\"\n  }\n}\n",
4918            ),
4919        ]);
4920        assert!(program.notices.is_empty());
4921    }
4922
4923    #[test]
4924    fn an_unknown_case_of_an_imported_enum_is_reported() {
4925        let diagnostic = resolve_err(
4926            &[
4927                (
4928                    "levels",
4929                    "/// Levels.\nexport enum LogLevel {\n  Debug\n  Info\n}\n",
4930                ),
4931                (
4932                    "app",
4933                    "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n  \
4934                     match level {\n    LogLevel.Debug => \"debug\"\n    LogLevel.Bogus => \"bogus\"\n    LogLevel.Info => \"info\"\n  }\n}\n",
4935                ),
4936            ],
4937            "cove::resolve::unknown_enum_case",
4938        );
4939        assert!(diagnostic.message.contains("Bogus"));
4940    }
4941
4942    // ------------------------------------------ conformances across modules
4943
4944    /// The trait, and the type, every cross-module conformance test builds
4945    /// on: one exported trait with a required and a defaulted method, and one
4946    /// exported struct, each in a module of its own.
4947    const DISPLAY: &str = "\
4948/// Renders itself.
4949export trait Display {
4950  /// The full form.
4951  fn describe(self) -> String
4952
4953  /// A short form, defaulting to the full one.
4954  fn label(self) -> String { self.describe() }
4955}
4956";
4957
4958    const BOOKING: &str = "\
4959/// A booking.
4960export struct Booking {
4961  id: Int
4962}
4963";
4964
4965    /// ADR 0006 allows a conformance in the module that declares the type,
4966    /// which with imports means the trait may be an imported one.
4967    #[test]
4968    fn a_module_may_conform_its_own_type_to_an_imported_trait() {
4969        let program = resolve_ok(&[
4970            ("display", DISPLAY),
4971            (
4972                "booking",
4973                &format!(
4974                    "use display.Display\n\n{BOOKING}\nimpl Display for Booking {{\n  \
4975                     /// The full form.\n  fn describe(self) -> String {{\n    \"b\"\n  }}\n}}\n"
4976                ),
4977            ),
4978        ]);
4979        let conformance = program.modules["booking"]
4980            .conformances
4981            .get(&("Display".to_string(), "Booking".to_string()))
4982            .expect("the conformance is recorded where the type is declared");
4983        assert_eq!(conformance.trait_module, "display");
4984        assert_eq!(conformance.type_module, "booking");
4985        // The defaulted method comes along, so dispatch finds both.
4986        assert_eq!(
4987            conformance.methods.iter().cloned().collect::<Vec<_>>(),
4988            ["describe", "label"]
4989        );
4990    }
4991
4992    /// And the reverse: a conformance in the module that declares the trait,
4993    /// for a type it imported.
4994    #[test]
4995    fn a_module_may_conform_an_imported_type_to_its_own_trait() {
4996        let program = resolve_ok(&[
4997            ("booking", BOOKING),
4998            (
4999                "display",
5000                &format!(
5001                    "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n  \
5002                     /// The full form.\n  fn describe(self) -> String {{\n    \"b\"\n  }}\n}}\n"
5003                ),
5004            ),
5005        ]);
5006        let conformance = program.modules["display"]
5007            .conformances
5008            .get(&("Display".to_string(), "Booking".to_string()))
5009            .expect("the conformance is recorded where the trait is declared");
5010        assert_eq!(conformance.trait_module, "display");
5011        assert_eq!(conformance.type_module, "booking");
5012        // The methods live with the conformance, not with the type.
5013        assert!(program.modules["display"]
5014            .methods
5015            .contains_key(&("Booking".to_string(), "describe".to_string())));
5016        assert!(program.modules["booking"].methods.is_empty());
5017    }
5018
5019    /// The orphan rule is what imports must *not* widen: a module that can
5020    /// see both parties still may not join them.
5021    #[test]
5022    fn a_third_module_may_not_conform_an_imported_type_to_an_imported_trait() {
5023        let diagnostic = resolve_err(
5024            &[
5025                ("display", DISPLAY),
5026                ("booking", BOOKING),
5027                (
5028                    "app",
5029                    "use display.Display\nuse booking.Booking\n\n\
5030                     impl Display for Booking {\n  /// The full form.\n  fn describe(self) -> String {\n    \"b\"\n  }\n}\n",
5031                ),
5032            ],
5033            "cove::resolve::orphan_conformance",
5034        );
5035        assert!(diagnostic.message.contains("declares neither"));
5036    }
5037
5038    /// An inherent `impl` is not a conformance, so it may not reach across a
5039    /// module boundary at all: there would be no fact for the type's own
5040    /// module to see.
5041    #[test]
5042    fn an_inherent_impl_may_not_extend_an_imported_type() {
5043        let diagnostic = resolve_err(
5044            &[
5045                ("booking", BOOKING),
5046                (
5047                    "app",
5048                    "use booking.Booking\n\nimpl Booking {\n  /// The id.\n  fn id(self) -> Int {\n    self.id\n  }\n}\n",
5049                ),
5050            ],
5051            "cove::resolve::foreign_inherent_impl",
5052        );
5053        assert!(diagnostic.help.as_deref().unwrap().contains("booking"));
5054    }
5055
5056    /// A conformance declared where the trait is may not collide with a
5057    /// method the type's own module declares: the checker would resolve one
5058    /// and the interpreter the other.
5059    #[test]
5060    fn a_conformance_may_not_collide_with_the_type_s_own_method() {
5061        let diagnostic = resolve_err(
5062            &[
5063                (
5064                    "booking",
5065                    &format!(
5066                        "{BOOKING}\nimpl Booking {{\n  /// Describes.\n  export fn describe(self) -> String {{\n    \"inherent\"\n  }}\n}}\n"
5067                    ),
5068                ),
5069                (
5070                    "display",
5071                    &format!(
5072                        "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n  \
5073                         /// The full form.\n  fn describe(self) -> String {{\n    \"conformance\"\n  }}\n}}\n"
5074                    ),
5075                ),
5076            ],
5077            "cove::resolve::duplicate_declaration",
5078        );
5079        assert!(diagnostic.message.contains("Booking.describe"));
5080        assert!(diagnostic.message.contains("display"));
5081        assert!(diagnostic.message.contains("booking"));
5082    }
5083
5084    /// The same collision between two conformances in two modules, which the
5085    /// per-module duplicate check cannot see either.
5086    #[test]
5087    fn two_modules_may_not_give_one_type_the_same_method_name() {
5088        let diagnostic = resolve_err(
5089            &[
5090                ("booking", BOOKING),
5091                (
5092                    "display",
5093                    &format!(
5094                        "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n  \
5095                         /// The full form.\n  fn describe(self) -> String {{\n    \"d\"\n  }}\n}}\n"
5096                    ),
5097                ),
5098                (
5099                    "audit",
5100                    "use booking.Booking\n\n\
5101                     /// Audits itself.\nexport trait Audit {\n  /// The full form.\n  fn describe(self) -> String\n}\n\n\
5102                     impl Audit for Booking {\n  /// The full form.\n  fn describe(self) -> String {\n    \"a\"\n  }\n}\n",
5103                ),
5104            ],
5105            "cove::resolve::duplicate_declaration",
5106        );
5107        assert!(diagnostic.message.contains("Booking.describe"));
5108    }
5109
5110    /// Two modules cannot both declare the same conformance, and the import
5111    /// rules are what guarantee it: each would have to import the other's
5112    /// party, which is a cycle. No separate check is needed, so this pins
5113    /// the shape that would need one if cycles were ever allowed.
5114    #[test]
5115    fn one_conformance_cannot_be_declared_in_both_parties_modules() {
5116        let diagnostic = resolve_err(
5117            &[
5118                (
5119                    "display",
5120                    &format!(
5121                        "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n  \
5122                         /// The full form.\n  fn describe(self) -> String {{\n    \"d\"\n  }}\n}}\n"
5123                    ),
5124                ),
5125                (
5126                    "booking",
5127                    &format!(
5128                        "use display.Display\n\n{BOOKING}\nimpl Display for Booking {{\n  \
5129                         /// The full form.\n  fn describe(self) -> String {{\n    \"b\"\n  }}\n}}\n"
5130                    ),
5131                ),
5132            ],
5133            "cove::resolve::import_cycle",
5134        );
5135        assert!(diagnostic.message.contains(" -> "));
5136    }
5137
5138    #[test]
5139    fn an_import_colliding_with_a_declared_trait_is_rejected() {
5140        let diagnostic = resolve_err(
5141            &[
5142                ("display", DISPLAY),
5143                (
5144                    "app",
5145                    "use display.Display\n\ntrait Display {\n  /// The full form.\n  fn describe(self) -> String\n}\n",
5146                ),
5147            ],
5148            "cove::resolve::import_conflict",
5149        );
5150        assert!(diagnostic.message.contains("also declares it"));
5151    }
5152
5153    #[test]
5154    fn a_use_of_a_private_trait_is_rejected() {
5155        resolve_err(
5156            &[
5157                (
5158                    "display",
5159                    "trait Display {\n  /// The full form.\n  fn describe(self) -> String\n}\n",
5160                ),
5161                ("app", "use display.Display\n"),
5162            ],
5163            "cove::resolve::private_declaration",
5164        );
5165    }
5166
5167    #[test]
5168    fn a_conformance_naming_a_trait_no_module_declares_is_rejected() {
5169        let diagnostic = resolve_err(
5170            &[(
5171                "booking",
5172                &format!("{BOOKING}\nimpl Display for Booking {{\n  fn describe(self) -> String {{\n    \"b\"\n  }}\n}}\n"),
5173            )],
5174            "cove::resolve::unknown_trait",
5175        );
5176        assert!(diagnostic.help.as_deref().unwrap().contains("use"));
5177    }
5178
5179    /// A conformance method is a method of the type wherever it was written,
5180    /// so a capability it needs reaches every caller of that method.
5181    #[test]
5182    fn required_capabilities_cross_a_conformance_in_another_module() {
5183        let program = resolve_ok(&[
5184            ("booking", BOOKING),
5185            (
5186                "display",
5187                &format!(
5188                    "use console.println\nuse booking.Booking\n\n{DISPLAY}\n\
5189                     impl Display for Booking {{\n  /// The full form.\n  fn describe(self) -> String {{\n    \
5190                     console.println(\"tracing\")\n    \"b\"\n  }}\n}}\n"
5191                ),
5192            ),
5193            (
5194                "app",
5195                "use booking.Booking\nuse display.Display\n\n\
5196                 /// Entry point.\nexport fn main(value: Booking) -> String {\n  value.describe()\n}\n",
5197            ),
5198        ]);
5199        assert!(program.modules["app"].functions["main"]
5200            .required_capabilities
5201            .contains(&Capability::new("console")));
5202    }
5203
5204    /// A bare case name resolves against the enums in scope, which now
5205    /// includes an imported one.
5206    #[test]
5207    fn a_bare_case_of_an_imported_enum_resolves() {
5208        let diagnostic = resolve_err(
5209            &[
5210                (
5211                    "levels",
5212                    "/// Levels.\nexport enum LogLevel {\n  Debug\n  Info\n}\n",
5213                ),
5214                (
5215                    "app",
5216                    "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n  \
5217                     match level {\n    Debug => \"debug\"\n  }\n}\n",
5218                ),
5219            ],
5220            "cove::resolve::non_exhaustive_match",
5221        );
5222        assert!(diagnostic.message.contains("LogLevel.Info"));
5223    }
5224
5225    #[test]
5226    fn ambiguous_unqualified_use_is_rejected() {
5227        let module =
5228            module_from_sources("ambiguous", &["use console.println\nuse other.println\n"]);
5229        let package = package_of(module);
5230        let errs = resolve(&package).unwrap_err();
5231        assert!(errs
5232            .iter()
5233            .any(|d| d.code == "cove::resolve::ambiguous_use"));
5234    }
5235
5236    #[test]
5237    fn derives_capability_from_qualified_call() {
5238        let module = module_from_sources(
5239            "cap",
5240            &[
5241                "use console.println\n\n/// Prints.\nexport fn main() {\n  console.println(\"hi\")\n}\n",
5242            ],
5243        );
5244        let package = package_of(module);
5245        let program = resolve(&package).expect("resolves");
5246        let entry = &program.modules["cap"].functions["main"];
5247        assert!(entry
5248            .direct_capabilities
5249            .contains(&Capability::new("console")));
5250    }
5251
5252    #[test]
5253    fn derives_capability_from_unqualified_call() {
5254        let module = module_from_sources(
5255            "cap2",
5256            &["use console.println\n\n/// Prints.\nexport fn main() {\n  println(\"hi\")\n}\n"],
5257        );
5258        let package = package_of(module);
5259        let program = resolve(&package).expect("resolves");
5260        let entry = &program.modules["cap2"].functions["main"];
5261        assert!(entry
5262            .direct_capabilities
5263            .contains(&Capability::new("console")));
5264    }
5265
5266    #[test]
5267    fn finds_a_host_call_inside_a_closure() {
5268        let module = module_from_sources(
5269            "cap3",
5270            &[
5271                "use console.println\n\n/// Builds a callback.\nexport fn build() {\n  let cb = fn() {\n    console.println(\"hi\")\n  }\n}\n",
5272            ],
5273        );
5274        let package = package_of(module);
5275        let program = resolve(&package).expect("resolves");
5276        let entry = &program.modules["cap3"].functions["build"];
5277        assert!(entry
5278            .direct_capabilities
5279            .contains(&Capability::new("console")));
5280    }
5281
5282    #[test]
5283    fn warns_on_missing_doc_for_exported_declaration() {
5284        let module = module_from_sources("nodoc", &["export fn main() {\n}\n"]);
5285        let package = package_of(module);
5286        let program = resolve(&package).expect("resolves even with a warning");
5287        assert!(program
5288            .notices
5289            .iter()
5290            .any(|d| d.code == "cove::resolve::missing_doc"));
5291    }
5292
5293    #[test]
5294    fn private_declaration_without_doc_does_not_warn() {
5295        let module = module_from_sources("private", &["fn helper() {\n}\n"]);
5296        let package = package_of(module);
5297        let program = resolve(&package).expect("resolves");
5298        assert!(program.notices.is_empty());
5299    }
5300
5301    #[test]
5302    fn loads_and_resolves_the_real_examples_package() {
5303        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples");
5304        let mut sources = SourceMap::new();
5305        let package = crate::package::load(&root, &mut sources).expect("examples package loads");
5306        let program = resolve(&package);
5307        assert!(program.is_ok(), "examples package should resolve cleanly");
5308    }
5309
5310    #[test]
5311    fn required_capabilities_reach_through_a_helper_chain() {
5312        let module = module_from_sources(
5313            "chain",
5314            &["use console.println\n\n\
5315                 /// Logs a message.\n\
5316                 fn log(msg: String) {\n  console.println(msg)\n}\n\n\
5317                 /// Entry point; never calls a Host API directly.\n\
5318                 export fn main() {\n  log(\"hi\")\n}\n"],
5319        );
5320        let package = package_of(module);
5321        let program = resolve(&package).expect("resolves");
5322        let main = &program.modules["chain"].functions["main"];
5323        assert!(main.direct_capabilities.is_empty());
5324        assert!(main
5325            .required_capabilities
5326            .contains(&Capability::new("console")));
5327    }
5328
5329    #[test]
5330    fn required_capabilities_reach_through_a_method_call() {
5331        let module = module_from_sources(
5332            "methodprop",
5333            &["use console.println\n\n\
5334                 /// A thing with an id.\n\
5335                 export struct Thing {\n  id: String\n}\n\n\
5336                 impl Thing {\n  \
5337                 /// Prints the id.\n  \
5338                 fn touch(self) {\n    console.println(self.id)\n  }\n}\n\n\
5339                 /// Entry point that reaches the Host API only through `Thing.touch`.\n\
5340                 export fn main() {\n  Thing.touch()\n}\n"],
5341        );
5342        let package = package_of(module);
5343        let program = resolve(&package).expect("resolves");
5344        let touch =
5345            &program.modules["methodprop"].methods[&("Thing".to_string(), "touch".to_string())];
5346        assert!(touch
5347            .direct_capabilities
5348            .contains(&Capability::new("console")));
5349        let main = &program.modules["methodprop"].functions["main"];
5350        assert!(main.direct_capabilities.is_empty());
5351        assert!(main
5352            .required_capabilities
5353            .contains(&Capability::new("console")));
5354    }
5355
5356    #[test]
5357    fn required_capabilities_propagate_through_mutual_recursion() {
5358        let module = module_from_sources(
5359            "mutual",
5360            &["use console.println\n\n\
5361                 /// True when `n` is even; recurses through `isOdd`.\n\
5362                 fn isEven(n: Int) -> Bool {\n  \
5363                 if n == 0 {\n    true\n  } else {\n    isOdd(n - 1)\n  }\n}\n\n\
5364                 /// True when `n` is odd; logs, then recurses through `isEven`.\n\
5365                 fn isOdd(n: Int) -> Bool {\n  \
5366                 console.println(\"checking\")\n  \
5367                 if n == 0 {\n    false\n  } else {\n    isEven(n - 1)\n  }\n}\n\n\
5368                 /// Entry point.\n\
5369                 export fn main() -> Bool {\n  isEven(4)\n}\n"],
5370        );
5371        let package = package_of(module);
5372        let program = resolve(&package).expect("resolves");
5373        let resolved = &program.modules["mutual"];
5374
5375        assert!(resolved.functions["isOdd"]
5376            .direct_capabilities
5377            .contains(&Capability::new("console")));
5378        assert!(resolved.functions["isEven"].direct_capabilities.is_empty());
5379
5380        // Neither function calls the other's host capability directly, but
5381        // the fixpoint must reach it through the recursive cycle without
5382        // looping forever.
5383        assert!(resolved.functions["isEven"]
5384            .required_capabilities
5385            .contains(&Capability::new("console")));
5386        assert!(resolved.functions["main"]
5387            .required_capabilities
5388            .contains(&Capability::new("console")));
5389    }
5390
5391    /// An embedder's schema for a module named after neither of its
5392    /// operations: the module's own capability is `directory`, but the
5393    /// `payroll` operation is gated on `payroll` instead. This is the shape
5394    /// the boundary (`HostRegistry::call_with`) actually enforces, and the
5395    /// checker must derive the same capability or an embedder could grant
5396    /// exactly what the checker asked for and still be refused at run time.
5397    const COMPANY: ModuleSchema = ModuleSchema {
5398        name: "company",
5399        capability: "directory",
5400        operations: &[
5401            OperationSchema {
5402                name: "employee",
5403                params: &[],
5404                variadic: false,
5405                result: HostType::Unit,
5406                capability: "directory",
5407                effect: Effect::Read,
5408                cancellable: false,
5409                recordable: true,
5410                result_is_task_safe: true,
5411            },
5412            OperationSchema {
5413                name: "payroll",
5414                params: &[],
5415                variadic: false,
5416                result: HostType::Unit,
5417                capability: "payroll",
5418                effect: Effect::Read,
5419                cancellable: false,
5420                recordable: true,
5421                result_is_task_safe: true,
5422            },
5423        ],
5424        types: &[],
5425        resources: &[],
5426    };
5427
5428    #[test]
5429    fn required_capabilities_use_the_operation_s_capability_not_the_module_s() {
5430        let schemas = HostSchemas::new().with(COMPANY);
5431        let program = resolve_ok_with(
5432            &[(
5433                "app",
5434                "use company\n\n/// Entry point.\nexport fn main() {\n  company.payroll()\n}\n",
5435            )],
5436            &schemas,
5437        );
5438        let main = &program.modules["app"].functions["main"];
5439        assert!(main
5440            .required_capabilities
5441            .contains(&Capability::new("payroll")));
5442        assert!(!main
5443            .required_capabilities
5444            .contains(&Capability::new("directory")));
5445    }
5446
5447    #[test]
5448    fn required_capabilities_fall_back_to_the_module_s_capability_for_an_undeclared_operation() {
5449        let schemas = HostSchemas::new().with(COMPANY);
5450        let program = resolve_ok_with(
5451            &[(
5452                "app",
5453                "use company\n\n/// Entry point; calls an operation the schema does not declare.\nexport fn main() {\n  company.other()\n}\n",
5454            )],
5455            &schemas,
5456        );
5457        let main = &program.modules["app"].functions["main"];
5458        assert!(main
5459            .required_capabilities
5460            .contains(&Capability::new("directory")));
5461        assert!(!main
5462            .required_capabilities
5463            .contains(&Capability::new("payroll")));
5464    }
5465
5466    #[test]
5467    fn a_function_requiring_nothing_stays_empty() {
5468        let module = module_from_sources(
5469            "pure",
5470            &[
5471                "/// Adds two numbers.\nfn add(a: Int, b: Int) -> Int {\n  a + b\n}\n\n\
5472                 /// Entry point; calls only a pure helper.\n\
5473                 export fn main() -> Int {\n  add(1, 2)\n}\n",
5474            ],
5475        );
5476        let package = package_of(module);
5477        let program = resolve(&package).expect("resolves");
5478        let resolved = &program.modules["pure"];
5479        assert!(resolved.functions["add"].required_capabilities.is_empty());
5480        assert!(resolved.functions["main"].required_capabilities.is_empty());
5481    }
5482
5483    #[test]
5484    fn derives_required_capabilities_for_the_real_examples_package() {
5485        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples");
5486        let mut sources = SourceMap::new();
5487        let package = crate::package::load(&root, &mut sources).expect("examples package loads");
5488        let program = resolve(&package).expect("examples package resolves");
5489
5490        let hello_main = &program.modules["hello"].functions["main"];
5491        assert!(hello_main
5492            .required_capabilities
5493            .contains(&Capability::new("console")));
5494
5495        let hello_greeting = &program.modules["hello"].functions["greeting"];
5496        assert!(hello_greeting.required_capabilities.is_empty());
5497
5498        let restricted_main = &program.modules["restricted"].functions["main"];
5499        assert!(restricted_main
5500            .required_capabilities
5501            .contains(&Capability::new("documents")));
5502        assert!(restricted_main
5503            .required_capabilities
5504            .contains(&Capability::new("console")));
5505
5506        let config_load_config = &program.modules["config"].functions["loadConfig"];
5507        assert!(config_load_config
5508            .required_capabilities
5509            .contains(&Capability::new("env")));
5510    }
5511
5512    // ------------------------------------------------ capability-openness
5513
5514    /// The declaration `module.name` of a resolved package.
5515    #[track_caller]
5516    fn function<'a>(program: &'a Program, module: &str, name: &str) -> &'a FnEntry {
5517        program
5518            .lookup_fn(module, name)
5519            .unwrap_or_else(|| panic!("`{module}.{name}` is declared"))
5520    }
5521
5522    #[test]
5523    fn calling_a_function_typed_parameter_is_capability_open() {
5524        let program = resolve_ok(&[(
5525            "higher",
5526            "/// Runs whatever it was handed.\n\
5527             export fn run(work: fn() -> Unit) {\n  work()\n}\n",
5528        )]);
5529        let run = function(&program, "higher", "run");
5530        assert!(run.required_capabilities.is_empty());
5531        assert_eq!(
5532            run.open_calls,
5533            BTreeSet::from([OpenCall::FunctionValue]),
5534            "a call to a value the call graph cannot name is the higher-order case"
5535        );
5536    }
5537
5538    /// The model this whole decision rests on: a lambda is charged to the
5539    /// function that *writes* it, so a closure invoked through a parameter
5540    /// does not lose its capability on the way -- while the function that
5541    /// invokes it is honest about not knowing what it will run.
5542    #[test]
5543    fn a_closure_that_calls_a_host_charges_the_function_that_wrote_it() {
5544        let program = resolve_ok(&[(
5545            "callback",
5546            "use console.println\n\n\
5547             /// Runs whatever it was handed.\n\
5548             fn run(work: fn() -> Unit) {\n  work()\n}\n\n\
5549             /// Hands `run` a closure that prints.\n\
5550             export fn main() {\n  run(fn() {\n    console.println(\"hi\")\n  })\n}\n",
5551        )]);
5552
5553        let main = function(&program, "callback", "main");
5554        assert!(
5555            main.direct_capabilities
5556                .contains(&Capability::new("console")),
5557            "the closure's body is part of the body that wrote it"
5558        );
5559
5560        let run = function(&program, "callback", "run");
5561        assert!(run.required_capabilities.is_empty());
5562        assert!(run.is_capability_open());
5563        assert_eq!(
5564            main.open_calls,
5565            BTreeSet::from([OpenCall::ReachedOpenCall]),
5566            "calling a capability-open declaration makes its caller one too"
5567        );
5568    }
5569
5570    #[test]
5571    fn calling_a_method_on_a_dyn_parameter_is_capability_open() {
5572        let program = resolve_ok(&[(
5573            "dynamic",
5574            "/// Something that describes itself.\n\
5575             export trait Summary {\n  \
5576             /// One line about this value.\n  \
5577             fn summarize(self) -> String\n}\n\n\
5578             /// Renders entries whose types may differ.\n\
5579             export fn report(entries: Array<dyn Summary>) -> String {\n  \
5580             var text = \"\"\n  \
5581             for entry in entries {\n    text = entry.summarize()\n  }\n  text\n}\n",
5582        )]);
5583        assert_eq!(
5584            function(&program, "dynamic", "report").open_calls,
5585            BTreeSet::from([OpenCall::DynamicDispatch]),
5586            "a `dyn` value taken out of a container still dispatches by its own type"
5587        );
5588    }
5589
5590    #[test]
5591    fn calling_a_method_on_a_bounded_generic_is_capability_open() {
5592        let program = resolve_ok(&[(
5593            "generic",
5594            "/// Something that describes itself.\n\
5595             export trait Summary {\n  \
5596             /// One line about this value.\n  \
5597             fn summarize(self) -> String\n}\n\n\
5598             /// Headlines one entry.\n\
5599             export fn headline<T: Summary>(entry: T) -> String {\n  entry.summarize()\n}\n",
5600        )]);
5601        assert_eq!(
5602            function(&program, "generic", "headline").open_calls,
5603            BTreeSet::from([OpenCall::DynamicDispatch]),
5604            "the caller instantiates `T`, so it also picks the conformance that runs"
5605        );
5606    }
5607
5608    #[test]
5609    fn calling_a_callback_stored_in_data_is_capability_open() {
5610        let program = resolve_ok(&[(
5611            "stored",
5612            "/// Runs every handler in turn.\n\
5613             export fn dispatch(handlers: Array<fn() -> Unit>) {\n  \
5614             for handler in handlers {\n    handler()\n  }\n}\n",
5615        )]);
5616        assert_eq!(
5617            function(&program, "stored", "dispatch").open_calls,
5618            BTreeSet::from([OpenCall::FunctionValue])
5619        );
5620    }
5621
5622    /// Everything a bare call can be other than a value: a declaration, a
5623    /// struct initializer, a host item, a free builtin, and a builtin type
5624    /// used as a namespace. None of them is indirect, and a report that
5625    /// called them open would be crying wolf on ordinary code.
5626    #[test]
5627    fn ordinary_calls_leave_a_function_capability_closed() {
5628        let program = resolve_ok(&[(
5629            "closed",
5630            "use console.println\n\n\
5631             /// A thing with an id.\n\
5632             export struct Thing {\n  id: String\n}\n\n\
5633             /// Makes one.\n\
5634             fn make() -> Thing {\n  Thing(id: \"a\")\n}\n\n\
5635             /// Entry point.\n\
5636             export fn main() -> Result<Unit, Error> {\n  \
5637             let thing = make()\n  \
5638             let items = Vector.of(thing.id)\n  \
5639             println(\"{items.length()}\")?\n  \
5640             assert(true)?\n  \
5641             Ok(())\n}\n",
5642        )]);
5643        let main = function(&program, "closed", "main");
5644        assert!(!main.is_capability_open(), "found {:?}", main.open_calls);
5645        assert!(main
5646            .required_capabilities
5647            .contains(&Capability::new("console")));
5648    }
5649
5650    /// A named function handed somewhere else to be called -- a route table,
5651    /// a host that will invoke it -- is still named, so the edge is real and
5652    /// the capability it needs reaches the function that named it.
5653    #[test]
5654    fn naming_a_function_as_a_value_reaches_what_it_requires() {
5655        let program = resolve_ok(&[(
5656            "reentry",
5657            "use http\n\
5658             use console.println\n\n\
5659             /// Answers one request, and says so on the console.\n\
5660             fn health(request: http.Request) -> http.Response {\n  \
5661             console.println(\"served\")\n  \
5662             http.json(200, \"ok\")\n}\n\n\
5663             /// Registers the handler the host will call back.\n\
5664             export fn routes() -> Array<http.Route> {\n  \
5665             [http.Route(method: http.Method.Get, path: \"/health\", handler: health)]\n}\n",
5666        )]);
5667        let routes = function(&program, "reentry", "routes");
5668        assert!(
5669            routes
5670                .required_capabilities
5671                .contains(&Capability::new("console")),
5672            "a callback the host will invoke is reached through the name that stored it"
5673        );
5674        assert!(
5675            !routes.is_capability_open(),
5676            "nothing here is a call the graph could not follow"
5677        );
5678    }
5679
5680    /// The shape that falsified the guarantee before the field types were
5681    /// read: a `dyn Trait` reached through a struct field rather than through
5682    /// a parameter. `lib` writes the type once, on the field, and never
5683    /// again; the conformance lives in `plugin`, which `lib` cannot reach, so
5684    /// the receiver over-approximation finds nothing either. Without the
5685    /// marker `app.main` reports an empty, complete-looking set and the run
5686    /// is refused at the boundary.
5687    #[test]
5688    fn dispatching_through_a_dyn_struct_field_is_capability_open() {
5689        let program = resolve_ok(&[
5690            (
5691                "lib",
5692                "/// Something that describes itself.\n\
5693                 export trait Summary {\n  \
5694                 /// One line about this value.\n  \
5695                 fn summarize(self) -> String\n}\n\n\
5696                 /// Holds one of them.\n\
5697                 export struct Box {\n  item: dyn Summary\n}\n\n\
5698                 impl Box {\n  \
5699                 /// Shows what it holds.\n  \
5700                 export fn show(self) -> String {\n    self.item.summarize()\n  }\n}\n",
5701            ),
5702            (
5703                "plugin",
5704                "use console.println\n\
5705                 use lib.Summary\n\n\
5706                 /// Says so out loud.\n\
5707                 export struct Noisy {\n  n: Int\n}\n\n\
5708                 impl Summary for Noisy {\n  \
5709                 /// One line about this value.\n  \
5710                 fn summarize(self) -> String {\n    \
5711                 let ignored = println(\"side effect\")\n    \"noisy\"\n  }\n}\n",
5712            ),
5713            (
5714                "app",
5715                "use lib\nuse plugin\n\n\
5716                 /// Entry point.\n\
5717                 export fn main() -> Result<Unit, Error> {\n  \
5718                 let held = lib.Box(item: plugin.Noisy(n: 1))\n  \
5719                 let text = held.show()\n  \
5720                 Ok(())\n}\n",
5721            ),
5722        ]);
5723        let show = &program.modules["lib"].methods[&("Box".to_string(), "show".to_string())];
5724        assert_eq!(
5725            show.open_calls,
5726            BTreeSet::from([OpenCall::DynamicDispatch]),
5727            "a `dyn` field is a value whose implementation its producer chose"
5728        );
5729        assert!(
5730            function(&program, "app", "main").is_capability_open(),
5731            "openness has to reach the entry, or its empty set reads as complete"
5732        );
5733    }
5734
5735    /// The container is not the thing it contains. `Array.length` is a
5736    /// builtin with no conformance to pick, so reading the element type at
5737    /// depth must not make the receiver itself opaque.
5738    #[test]
5739    fn a_method_on_a_container_of_generics_is_not_dynamic_dispatch() {
5740        let program = resolve_ok(&[(
5741            "counting",
5742            "/// How many entries there are.\n\
5743             export fn count<T>(items: Array<T>) -> Int {\n  items.length()\n}\n",
5744        )]);
5745        let count = function(&program, "counting", "count");
5746        assert!(!count.is_capability_open(), "found {:?}", count.open_calls);
5747    }
5748
5749    /// ...while what comes *out* of that container still is.
5750    #[test]
5751    fn a_method_on_an_element_of_a_dyn_container_is_dynamic_dispatch() {
5752        let program = resolve_ok(&[(
5753            "element",
5754            "/// Something that describes itself.\n\
5755             export trait Summary {\n  \
5756             /// One line about this value.\n  \
5757             fn summarize(self) -> String\n}\n\n\
5758             /// The first entry's line, or nothing.\n\
5759             export fn first(entries: Array<dyn Summary>) -> String {\n  \
5760             entries.get(0).map(fn(entry) {\n    entry.summarize()\n  }).unwrapOr(\"\")\n}\n",
5761        )]);
5762        assert_eq!(
5763            function(&program, "element", "first").open_calls,
5764            BTreeSet::from([OpenCall::DynamicDispatch]),
5765            "an element taken out of a `dyn` container dispatches by its own type"
5766        );
5767    }
5768
5769    /// A name the body binds is that name, whatever the module declares
5770    /// under it. Reading it recorded an exact call-graph edge before, which
5771    /// charged a pure function a capability it cannot reach.
5772    #[test]
5773    fn a_parameter_shadowing_a_function_records_no_edge() {
5774        let program = resolve_ok(&[(
5775            "shadow",
5776            "use console.println\n\n\
5777             /// Prints one line.\n\
5778             fn report(text: String) -> Result<Unit, Error> {\n  println(text)\n}\n\n\
5779             /// Returns what it was given.\n\
5780             export fn label(report: String) -> String {\n  report\n}\n",
5781        )]);
5782        let label = function(&program, "shadow", "label");
5783        assert!(
5784            label.required_capabilities.is_empty(),
5785            "found {:?}",
5786            label.required_capabilities
5787        );
5788        assert!(!label.is_capability_open(), "found {:?}", label.open_calls);
5789    }
5790
5791    /// A local `fn` is an ordinary closure, so it is charged where it is
5792    /// written -- which both restores the capability its body needs and
5793    /// removes the `FunctionValue` marker calling it used to earn.
5794    #[test]
5795    fn a_local_fn_is_charged_to_the_body_that_wrote_it() {
5796        let program = resolve_ok(&[(
5797            "local",
5798            "use console.println\n\n\
5799             /// Entry point.\n\
5800             export fn main() -> Result<Unit, Error> {\n  \
5801             /// Prints once.\n  \
5802             fn helper() -> Result<Unit, Error> {\n    println(\"hi\")\n  }\n  \
5803             helper()?\n  Ok(())\n}\n",
5804        )]);
5805        let main = function(&program, "local", "main");
5806        assert!(main
5807            .required_capabilities
5808            .contains(&Capability::new("console")));
5809        assert!(!main.is_capability_open(), "found {:?}", main.open_calls);
5810    }
5811
5812    #[test]
5813    fn openness_crosses_a_module_boundary() {
5814        let program = resolve_ok(&[
5815            (
5816                "runner",
5817                "/// Runs whatever it was handed.\n\
5818                 export fn run(work: fn() -> Unit) {\n  work()\n}\n",
5819            ),
5820            (
5821                "app",
5822                "use runner.run\n\n\
5823                 /// Entry point.\n\
5824                 export fn main() {\n  run(fn() {\n  })\n}\n",
5825            ),
5826        ]);
5827        assert!(function(&program, "app", "main").is_capability_open());
5828    }
5829
5830    #[test]
5831    fn match_covering_every_enum_case_passes() {
5832        let module = module_from_sources(
5833            "exhaustive",
5834            &["enum LogLevel {\n  Debug\n  Info\n}\n\n\
5835               fn describe(level: LogLevel) -> String {\n  \
5836               match level {\n    \
5837               LogLevel.Debug => \"debug\"\n    \
5838               LogLevel.Info => \"info\"\n  \
5839               }\n}\n"],
5840        );
5841        let package = package_of(module);
5842        let program = resolve(&package).expect("resolves");
5843        assert!(program.notices.is_empty());
5844    }
5845
5846    #[test]
5847    fn missing_case_is_reported_by_name() {
5848        let module = module_from_sources(
5849            "missing",
5850            &["enum LogLevel {\n  Debug\n  Info\n  Warn\n  Error\n}\n\n\
5851               fn describe(level: LogLevel) -> String {\n  \
5852               match level {\n    \
5853               LogLevel.Debug => \"debug\"\n    \
5854               LogLevel.Info => \"info\"\n  \
5855               }\n}\n"],
5856        );
5857        let package = package_of(module);
5858        let errs = resolve(&package).unwrap_err();
5859        let diag = errs
5860            .iter()
5861            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
5862            .expect("reports non_exhaustive_match");
5863        assert!(diag.message.contains("LogLevel.Warn"));
5864        assert!(diag.message.contains("LogLevel.Error"));
5865        assert!(diag.help.as_deref().unwrap().contains("LogLevel.Warn"));
5866    }
5867
5868    #[test]
5869    fn a_wildcard_arm_makes_a_partial_match_exhaustive() {
5870        let module = module_from_sources(
5871            "wildcard_ok",
5872            &["enum LogLevel {\n  Debug\n  Info\n  Warn\n  Error\n}\n\n\
5873               fn describe(level: LogLevel) -> String {\n  \
5874               match level {\n    \
5875               LogLevel.Debug => \"debug\"\n    \
5876               _ => \"other\"\n  \
5877               }\n}\n"],
5878        );
5879        let package = package_of(module);
5880        let program = resolve(&package).expect("resolves");
5881        assert!(!program
5882            .notices
5883            .iter()
5884            .any(|d| d.code == "cove::resolve::non_exhaustive_match"));
5885    }
5886
5887    #[test]
5888    fn option_match_covering_both_cases_passes() {
5889        let module = module_from_sources(
5890            "option_ok",
5891            &["fn describe(value: Option<Int>) -> Int {\n  \
5892               match value {\n    \
5893               Some(x) => x\n    \
5894               None => 0\n  \
5895               }\n}\n"],
5896        );
5897        let package = package_of(module);
5898        resolve(&package).expect("resolves");
5899    }
5900
5901    #[test]
5902    fn option_match_missing_none_is_reported() {
5903        let module = module_from_sources(
5904            "option_missing",
5905            &["fn describe(value: Option<Int>) -> Int {\n  \
5906               match value {\n    \
5907               Some(x) => x\n  \
5908               }\n}\n"],
5909        );
5910        let package = package_of(module);
5911        let errs = resolve(&package).unwrap_err();
5912        let diag = errs
5913            .iter()
5914            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
5915            .expect("reports non_exhaustive_match");
5916        assert!(diag.message.contains("None"));
5917    }
5918
5919    #[test]
5920    fn result_match_covering_both_cases_passes() {
5921        let module = module_from_sources(
5922            "result_ok",
5923            &["fn describe(value: Result<Int, Error>) -> Int {\n  \
5924               match value {\n    \
5925               Ok(x) => x\n    \
5926               Err(e) => 0\n  \
5927               }\n}\n"],
5928        );
5929        let package = package_of(module);
5930        resolve(&package).expect("resolves");
5931    }
5932
5933    #[test]
5934    fn result_match_missing_err_is_reported() {
5935        let module = module_from_sources(
5936            "result_missing",
5937            &["fn describe(value: Result<Int, Error>) -> Int {\n  \
5938               match value {\n    \
5939               Ok(x) => x\n  \
5940               }\n}\n"],
5941        );
5942        let package = package_of(module);
5943        let errs = resolve(&package).unwrap_err();
5944        let diag = errs
5945            .iter()
5946            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
5947            .expect("reports non_exhaustive_match");
5948        assert!(diag.message.contains("Err"));
5949    }
5950
5951    #[test]
5952    fn unknown_enum_case_is_reported() {
5953        let module = module_from_sources(
5954            "unknown_case",
5955            &["enum LogLevel {\n  Debug\n  Info\n}\n\n\
5956               fn describe(level: LogLevel) -> String {\n  \
5957               match level {\n    \
5958               LogLevel.Debug => \"debug\"\n    \
5959               LogLevel.Bogus => \"bogus\"\n  \
5960               }\n}\n"],
5961        );
5962        let package = package_of(module);
5963        let errs = resolve(&package).unwrap_err();
5964        assert!(errs
5965            .iter()
5966            .any(|d| d.code == "cove::resolve::unknown_enum_case"));
5967    }
5968
5969    #[test]
5970    fn duplicate_match_arm_is_reported() {
5971        let module = module_from_sources(
5972            "dup_arm",
5973            &["enum LogLevel {\n  Debug\n  Info\n}\n\n\
5974               fn describe(level: LogLevel) -> String {\n  \
5975               match level {\n    \
5976               LogLevel.Debug => \"first\"\n    \
5977               LogLevel.Debug => \"second\"\n    \
5978               LogLevel.Info => \"info\"\n  \
5979               }\n}\n"],
5980        );
5981        let package = package_of(module);
5982        let errs = resolve(&package).unwrap_err();
5983        assert!(errs
5984            .iter()
5985            .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
5986    }
5987
5988    /// Two arms naming one case are not duplicates when their sub-patterns
5989    /// disagree: `Some(Json.Text(value))` matches only a `Some` holding a
5990    /// `Json.Text`, so the `Some(other)` after it is reachable, and both
5991    /// arms bind. Only a sub-pattern that matches everything makes the case
5992    /// covered.
5993    #[test]
5994    fn a_narrower_sub_pattern_does_not_cover_its_whole_case() {
5995        let module = module_from_sources(
5996            "nested_sub_pattern",
5997            &["enum Json {\n  Text(String)\n  Number(Int)\n}\n\n\
5998               fn describe(entry: Option<Json>) -> String {\n  \
5999               match entry {\n    \
6000               None => \"none\"\n    \
6001               Some(Json.Text(value)) => value\n    \
6002               Some(other) => \"other\"\n  \
6003               }\n}\n"],
6004        );
6005        let package = package_of(module);
6006        let program = resolve(&package).expect("resolves: the arms do not overlap");
6007        assert!(!program
6008            .notices
6009            .iter()
6010            .any(|d| d.code == "cove::resolve::unreachable_match_arm"));
6011    }
6012
6013    /// A binding sub-pattern *is* a catch-all for its case, so a second
6014    /// `Some` arm after one is the dead code the rule exists to catch.
6015    #[test]
6016    fn a_binding_sub_pattern_covers_its_whole_case() {
6017        let module = module_from_sources(
6018            "binding_sub_pattern",
6019            &["fn describe(entry: Option<Int>) -> String {\n  \
6020               match entry {\n    \
6021               None => \"none\"\n    \
6022               Some(first) => \"first\"\n    \
6023               Some(second) => \"second\"\n  \
6024               }\n}\n"],
6025        );
6026        let package = package_of(module);
6027        let errs = resolve(&package).unwrap_err();
6028        assert!(errs
6029            .iter()
6030            .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
6031    }
6032
6033    /// The distinction recurses: two arms whose sub-patterns are themselves
6034    /// the same pattern are still a duplicate, however deep the agreement
6035    /// goes.
6036    #[test]
6037    fn identical_sub_patterns_are_still_a_duplicate() {
6038        let module = module_from_sources(
6039            "identical_sub_pattern",
6040            &["enum Json {\n  Text(String)\n  Number(Int)\n}\n\n\
6041               fn describe(entry: Option<Json>) -> String {\n  \
6042               match entry {\n    \
6043               None => \"none\"\n    \
6044               Some(Json.Text(first)) => first\n    \
6045               Some(Json.Text(second)) => second\n  \
6046               }\n}\n"],
6047        );
6048        let package = package_of(module);
6049        let errs = resolve(&package).unwrap_err();
6050        assert!(errs
6051            .iter()
6052            .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
6053    }
6054
6055    #[test]
6056    fn arm_after_a_wildcard_is_an_unreachable_warning() {
6057        let module = module_from_sources(
6058            "unreachable_arm",
6059            &["fn tag(n: Int) -> String {\n  \
6060               match n {\n    \
6061               _ => \"any\"\n    \
6062               1 => \"one\"\n  \
6063               }\n}\n"],
6064        );
6065        let package = package_of(module);
6066        let program = resolve(&package).expect("resolves; only a warning");
6067        assert!(program
6068            .notices
6069            .iter()
6070            .any(|d| d.code == "cove::resolve::unreachable_match_arm"));
6071    }
6072
6073    #[test]
6074    fn literal_match_over_non_bool_without_a_catch_all_arm_is_reported() {
6075        let module = module_from_sources(
6076            "literal_missing",
6077            &["fn tag(n: Int) -> String {\n  \
6078               match n {\n    \
6079               1 => \"one\"\n    \
6080               2 => \"two\"\n  \
6081               }\n}\n"],
6082        );
6083        let package = package_of(module);
6084        let errs = resolve(&package).unwrap_err();
6085        let diag = errs
6086            .iter()
6087            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6088            .expect("reports non_exhaustive_match");
6089        assert!(diag.message.contains("literal"));
6090    }
6091
6092    /// `Bool`'s domain is exactly `{true, false}`, so covering both makes the
6093    /// match exhaustive without a catch-all — unlike `Int` or `String`.
6094    #[test]
6095    fn bool_match_covering_both_values_passes_without_a_catch_all() {
6096        let module = module_from_sources(
6097            "bool_ok",
6098            &["fn flag(on: Bool) -> String {\n  \
6099               match on {\n    \
6100               true => \"yes\"\n    \
6101               false => \"no\"\n  \
6102               }\n}\n"],
6103        );
6104        let package = package_of(module);
6105        let program = resolve(&package).expect("resolves");
6106        assert!(!program
6107            .notices
6108            .iter()
6109            .any(|d| d.code == "cove::resolve::non_exhaustive_match"));
6110    }
6111
6112    #[test]
6113    fn bool_match_missing_false_is_reported_by_name() {
6114        let module = module_from_sources(
6115            "bool_missing_false",
6116            &["fn flag(on: Bool) -> String {\n  \
6117               match on {\n    \
6118               true => \"yes\"\n  \
6119               }\n}\n"],
6120        );
6121        let package = package_of(module);
6122        let errs = resolve(&package).unwrap_err();
6123        let diag = errs
6124            .iter()
6125            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6126            .expect("reports non_exhaustive_match");
6127        assert!(diag.message.contains("`false`"));
6128        assert!(diag.help.as_deref().unwrap().contains("false"));
6129    }
6130
6131    #[test]
6132    fn bool_match_missing_true_is_reported_by_name() {
6133        let module = module_from_sources(
6134            "bool_missing_true",
6135            &["fn flag(on: Bool) -> String {\n  \
6136               match on {\n    \
6137               false => \"no\"\n  \
6138               }\n}\n"],
6139        );
6140        let package = package_of(module);
6141        let errs = resolve(&package).unwrap_err();
6142        let diag = errs
6143            .iter()
6144            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6145            .expect("reports non_exhaustive_match");
6146        assert!(diag.message.contains("`true`"));
6147        assert!(diag.help.as_deref().unwrap().contains("true"));
6148    }
6149
6150    #[test]
6151    fn bool_match_with_both_values_and_a_wildcard_warns_the_wildcard_is_unreachable() {
6152        let module = module_from_sources(
6153            "bool_wildcard_unreachable",
6154            &["fn flag(on: Bool) -> String {\n  \
6155               match on {\n    \
6156               true => \"yes\"\n    \
6157               false => \"no\"\n    \
6158               _ => \"other\"\n  \
6159               }\n}\n"],
6160        );
6161        let package = package_of(module);
6162        let program = resolve(&package).expect("resolves; only a warning");
6163        assert!(program
6164            .notices
6165            .iter()
6166            .any(|d| d.code == "cove::resolve::unreachable_match_arm"));
6167    }
6168
6169    #[test]
6170    fn duplicate_bool_match_arm_is_reported() {
6171        let module = module_from_sources(
6172            "bool_dup_arm",
6173            &["fn flag(on: Bool) -> String {\n  \
6174               match on {\n    \
6175               true => \"yes\"\n    \
6176               true => \"also yes\"\n    \
6177               false => \"no\"\n  \
6178               }\n}\n"],
6179        );
6180        let package = package_of(module);
6181        let errs = resolve(&package).unwrap_err();
6182        assert!(errs
6183            .iter()
6184            .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
6185    }
6186
6187    /// A literal `match` mixing a `Bool` arm with a non-`Bool` literal is not
6188    /// exhaustible by the `Bool`-domain rule, so it keeps needing a
6189    /// catch-all like any other literal match.
6190    #[test]
6191    fn mixed_bool_and_int_literal_match_still_needs_a_catch_all() {
6192        let module = module_from_sources(
6193            "mixed_literal",
6194            &["fn describe(n: Int) -> String {\n  \
6195               match n {\n    \
6196               true => \"true?\"\n    \
6197               1 => \"one\"\n  \
6198               }\n}\n"],
6199        );
6200        let package = package_of(module);
6201        let errs = resolve(&package).unwrap_err();
6202        let diag = errs
6203            .iter()
6204            .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6205            .expect("reports non_exhaustive_match");
6206        assert!(diag.message.contains("literal"));
6207    }
6208
6209    /// Pins the shape used by `examples/config/load.cove`: string literals
6210    /// with a final binding arm that catches everything else.
6211    #[test]
6212    fn literal_match_with_a_binding_arm_passes() {
6213        let module = module_from_sources(
6214            "literal_ok",
6215            &["fn parseLevel(raw: String) -> String {\n  \
6216               match raw {\n    \
6217               \"debug\" => \"Debug\"\n    \
6218               \"info\" => \"Info\"\n    \
6219               other => other\n  \
6220               }\n}\n"],
6221        );
6222        let package = package_of(module);
6223        let program = resolve(&package).expect("resolves");
6224        assert!(!program
6225            .notices
6226            .iter()
6227            .any(|d| d.code == "cove::resolve::non_exhaustive_match"));
6228    }
6229
6230    #[test]
6231    fn a_match_whose_enum_is_ambiguous_stays_silent() {
6232        let module = module_from_sources(
6233            "ambiguous_enum",
6234            &["enum Left {\n  A\n  B\n}\n\n\
6235               enum Right {\n  A\n  C\n}\n\n\
6236               fn pick(x: Int) -> Int {\n  \
6237               match x {\n    \
6238               A => 1\n    \
6239               B => 2\n  \
6240               }\n}\n"],
6241        );
6242        let package = package_of(module);
6243        let program = resolve(&package).expect("resolves; the enum cannot be determined");
6244        assert!(!program
6245            .notices
6246            .iter()
6247            .any(|d| d.code.starts_with("cove::resolve::") && d.code.contains("match")));
6248    }
6249
6250    #[test]
6251    fn break_and_continue_inside_a_loop_resolve_cleanly() {
6252        let module = module_from_sources(
6253            "loop_ok",
6254            &["fn firstEven(items: Int...) -> Int {\n  \
6255               for item in items {\n    \
6256               if item % 2 != 0 {\n      continue\n    }\n    \
6257               break item\n  \
6258               }\n}\n"],
6259        );
6260        let package = package_of(module);
6261        resolve(&package).expect("resolves");
6262    }
6263
6264    #[test]
6265    fn break_outside_a_loop_is_rejected() {
6266        let module = module_from_sources("break_bare", &["fn go() {\n  break\n}\n"]);
6267        let package = package_of(module);
6268        let errs = resolve(&package).unwrap_err();
6269        assert!(errs
6270            .iter()
6271            .any(|d| d.code == "cove::resolve::break_outside_loop"));
6272    }
6273
6274    #[test]
6275    fn continue_outside_a_loop_is_rejected() {
6276        let module = module_from_sources("continue_bare", &["fn go() {\n  continue\n}\n"]);
6277        let package = package_of(module);
6278        let errs = resolve(&package).unwrap_err();
6279        assert!(errs
6280            .iter()
6281            .any(|d| d.code == "cove::resolve::continue_outside_loop"));
6282    }
6283
6284    #[test]
6285    fn break_inside_a_lambda_cannot_reach_an_outer_loop() {
6286        let module = module_from_sources(
6287            "break_in_lambda",
6288            &["fn go() {\n  for item in [1, 2] {\n    \
6289               let f = fn() {\n      break\n    }\n  \
6290               }\n}\n"],
6291        );
6292        let package = package_of(module);
6293        let errs = resolve(&package).unwrap_err();
6294        assert!(errs
6295            .iter()
6296            .any(|d| d.code == "cove::resolve::break_outside_loop"));
6297    }
6298}
6299
6300#[cfg(test)]
6301mod send_sync {
6302    use super::Program;
6303
6304    /// A task thread runs the same resolved program as the thread that
6305    /// spawned it, reached by reference, so the program must be shareable
6306    /// across threads (ADR 0008). Nothing in a resolved program is mutable,
6307    /// so this holds as long as no reference-counted handle in it is `Rc`.
6308    #[test]
6309    fn a_resolved_program_is_shareable_across_task_threads() {
6310        fn assert_send_sync<T: Send + Sync>() {}
6311        assert_send_sync::<Program>();
6312    }
6313}