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