Skip to main content

submilli_engine/typechecker/rules/
mod.rs

1//! Check (Rules) pass — semantic rules over the Typed AST.
2//!
3//! Read-only: produces additional diagnostics, never modifies the tree.
4
5pub(crate) mod body_walk;
6mod capability_consistency;
7mod check_calls;
8mod check_discipline;
9mod control_flow;
10mod declarations;
11mod definite_assignment;
12mod doc_consistency;
13mod exported_docs;
14pub(crate) mod exported_signature_types;
15mod fallthrough;
16mod main_required;
17mod missing_return;
18mod return_outside_function;
19mod super_call;
20mod unreachable;
21
22use crate::compiler_error::{CompileError, CompilerFailure, CompilerStage};
23use crate::{Diagnostic, ExportEntry, PackageDeclaration, Type, TypedAst, tree_height};
24
25use super::infer::module_symbols::ModuleSymbols;
26use declarations::TypeDeclarations;
27
28/// [`check_script`] for a script whose types are all its own: one imported
29/// from a package cannot be resolved here.
30pub fn check(ta: &TypedAst) -> Result<Vec<Diagnostic>, CompileError> {
31    check_script(ta, &[])
32}
33
34/// Checks a script. `dependencies` are the packages it was inferred against,
35/// which declare the types it imports.
36pub fn check_script(
37    ta: &TypedAst,
38    dependencies: &[&PackageDeclaration],
39) -> Result<Vec<Diagnostic>, CompileError> {
40    tree_height::check_typed(ta, CompilerStage::Infer)?;
41    let declarations = TypeDeclarations::for_script(ta, dependencies);
42    let mut diags = Vec::new();
43    run_rules(ta, &declarations, Source::Script, &mut diags).map_err(|fatal| CompileError {
44        diagnostics: diags.clone(),
45        fatal: Some(fatal),
46    })?;
47    Ok(diags)
48}
49
50/// The type a `@capability` binding path reads from a parameter of type `ty`
51/// in `package`, resolved as the capability-consistency rule resolves it.
52/// `None` when a segment names no field or reaches an unknown declaration.
53pub fn capability_binding_type(
54    package: &PackageDeclaration,
55    dependencies: &[&PackageDeclaration],
56    ty: &Type,
57    path: &[String],
58) -> Option<Type> {
59    let declarations = TypeDeclarations::for_package(package, dependencies);
60    match capability_consistency::binding_target(&declarations, ty, path) {
61        capability_consistency::BindingTarget::Found(ty) => Some(ty),
62        capability_consistency::BindingTarget::Missing(_)
63        | capability_consistency::BindingTarget::Unresolved => None,
64    }
65}
66
67pub(in crate::typechecker) struct PackageModuleSurface<'a> {
68    pub(in crate::typechecker) symbols: &'a ModuleSymbols,
69    pub(in crate::typechecker) exports: &'a [ExportEntry],
70}
71
72/// Checks a package. `dependencies` are the packages it was inferred against,
73/// the ones it cannot import from included.
74pub(in crate::typechecker) fn check_package<'a>(
75    package_name: &str,
76    typed_ast: &TypedAst,
77    package: &PackageDeclaration,
78    exports: &[ExportEntry],
79    modules: impl IntoIterator<Item = PackageModuleSurface<'a>>,
80    dependencies: &[&PackageDeclaration],
81) -> Result<Vec<Diagnostic>, CompileError> {
82    tree_height::check_typed(typed_ast, CompilerStage::Infer)?;
83    let mut diags = Vec::new();
84    for module in modules {
85        exported_signature_types::run_module(
86            package_name,
87            "module",
88            module.symbols,
89            module.exports,
90            &mut diags,
91        );
92    }
93    exported_signature_types::run(package_name, "package", package, exports, &mut diags);
94    exported_docs::run(typed_ast, package, exports, &mut diags);
95    let declarations = TypeDeclarations::for_package(package, dependencies);
96    run_rules(typed_ast, &declarations, Source::Package, &mut diags)
97        .and_then(|()| check_discipline::run(typed_ast, &mut diags))
98        .map_err(|fatal| CompileError {
99            diagnostics: diags.clone(),
100            fatal: Some(fatal),
101        })?;
102    Ok(diags)
103}
104
105#[derive(Clone, Copy, PartialEq, Eq)]
106enum Source {
107    Script,
108    Package,
109}
110
111/// The rules a script and a package share, in the order they report.
112fn run_rules(
113    ta: &TypedAst,
114    declarations: &TypeDeclarations<'_>,
115    source: Source,
116    diags: &mut Vec<Diagnostic>,
117) -> Result<(), CompilerFailure> {
118    missing_return::run(ta, declarations, diags)?;
119    unreachable::run(ta, declarations, diags)?;
120    return_outside_function::run(ta, diags)?;
121    fallthrough::run(ta, declarations, diags)?;
122    definite_assignment::run(ta, diags)?;
123    super_call::run(ta, diags)?;
124    if source == Source::Script {
125        // Inference reports a package that declares `main`.
126        main_required::run(ta, diags);
127    }
128    doc_consistency::run(ta, diags)?;
129    capability_consistency::run(ta, declarations, diags)
130}
131
132#[cfg(test)]
133mod test_util {
134    use std::collections::BTreeMap;
135
136    use super::check_script;
137    use crate::{
138        Asi, Ast, Diagnostic, FileId, ModulePath, PackageDeclaration, Sources, Token, TokenKind,
139        TypedAst, infer, infer_package, lower_patterns, parse,
140    };
141
142    /// Most rule tests aren't about `main` — prefix the source with a no-op
143    /// `main` so the missing-main diagnostic doesn't pollute their counts.
144    pub fn run(source: &str) -> Vec<Diagnostic> {
145        let with_main = format!("function main(): void {{ }}\n{source}");
146        pipeline(&with_main)
147    }
148
149    pub fn run_raw(source: &str) -> Vec<Diagnostic> {
150        pipeline(source)
151    }
152
153    /// Each diagnostic of [`run_raw`] as its message and 1-based line, so a
154    /// test can tell apart several reports that share a message.
155    pub fn run_lines(source: &str) -> Vec<(String, usize)> {
156        run_raw(source)
157            .into_iter()
158            .map(|diag| {
159                let line = source[..diag.span.start as usize].lines().count().max(1);
160                (diag.message, line)
161            })
162            .collect()
163    }
164
165    /// Infers a script against the runtime and the standard library, without
166    /// checking it.
167    pub fn infer_script(source: &str) -> (TypedAst, Vec<Diagnostic>) {
168        infer_script_with(source, &[])
169    }
170
171    /// [`infer_script`] with `packages` to import from as well.
172    pub fn infer_script_with(
173        source: &str,
174        packages: &[PackageDeclaration],
175    ) -> (TypedAst, Vec<Diagnostic>) {
176        let ast = parse_source(source, FileId(0));
177        let dependencies = runtime_declarations();
178        let dependencies: Vec<_> = dependencies.iter().chain(packages).collect();
179        infer(source, "main", &ast, &dependencies)
180    }
181
182    /// Infers and checks the package `modules` make up, rooted at `lib`.
183    pub fn run_package(
184        name: &str,
185        modules: &[(&str, &str)],
186        dependencies: &[PackageDeclaration],
187    ) -> (TypedAst, PackageDeclaration, Vec<Diagnostic>) {
188        let mut sources = Sources::new();
189        let parsed: Vec<(ModulePath, FileId, Ast)> = modules
190            .iter()
191            .map(|(module, source)| {
192                let file = sources.add((*module).to_string(), *source).unwrap();
193                let ast = lower_patterns(parse_source(source, file)).unwrap();
194                (ModulePath::from(*module), file, ast)
195            })
196            .collect();
197        let external: BTreeMap<String, PackageDeclaration> = runtime_declarations()
198            .into_iter()
199            .chain(dependencies.iter().cloned())
200            .map(|declaration| (declaration.package_name.clone(), declaration))
201            .collect();
202        infer_package(
203            name,
204            ModulePath::from("lib"),
205            parsed
206                .iter()
207                .map(|(module, file, ast)| (module.clone(), *file, ast))
208                .collect(),
209            &sources,
210            external,
211            BTreeMap::new(),
212        )
213    }
214
215    fn pipeline(source: &str) -> Vec<Diagnostic> {
216        let ast = parse_source(source, FileId(0));
217        let (prelude_defs, host_defs, _) =
218            crate::runtime::prelude::cached_runtime_package_declarations();
219        let mut packages = Vec::with_capacity(prelude_defs.len() + host_defs.len());
220        packages.extend(prelude_defs.iter());
221        packages.extend(host_defs.iter());
222        let (ta, mut diags) = infer(source, "main", &ast, &packages);
223        diags.extend(check_script(&ta, &packages).unwrap());
224        diags
225    }
226
227    fn parse_source(source: &str, file: FileId) -> Ast {
228        let mut asi = Asi::new(source, file);
229        let mut tokens: Vec<Token> = Vec::new();
230        loop {
231            let tok = asi.next_token();
232            let is_eof = matches!(tok.kind, TokenKind::Eof);
233            tokens.push(tok);
234            if is_eof {
235                break;
236            }
237        }
238        let lex_diags = asi.into_diagnostics();
239        assert!(
240            lex_diags.is_empty(),
241            "unexpected lexer diags: {lex_diags:?}"
242        );
243        let (ast, parse_diags) = parse(source, tokens, file);
244        assert!(
245            parse_diags.is_empty(),
246            "unexpected parser diags: {parse_diags:?}"
247        );
248        ast
249    }
250
251    fn runtime_declarations() -> Vec<PackageDeclaration> {
252        let (prelude_defs, host_defs, _) =
253            crate::runtime::prelude::cached_runtime_package_declarations();
254        prelude_defs
255            .iter()
256            .chain(host_defs)
257            .cloned()
258            .chain(crate::stdlib::stdlib_package_declarations())
259            .collect()
260    }
261}