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    for module in modules {
99        exported_signature_types::run_module(
100            package_name,
101            "module",
102            module.symbols,
103            module.exports,
104            &mut diags,
105        );
106    }
107    exported_signature_types::run(package_name, "package", package, exports, &mut diags);
108    exported_docs::run(typed_ast, package, exports, &mut diags);
109    let declarations = TypeDeclarations::for_package(package, dependencies);
110    run_rules(typed_ast, &declarations, Source::Package, &mut diags)
111        .and_then(|()| check_discipline::run(typed_ast, &mut diags))
112        .map_err(|fatal| CompileError {
113            diagnostics: diags.clone(),
114            fatal: Some(fatal),
115        })?;
116    Ok(diags)
117}
118
119#[derive(Clone, Copy, PartialEq, Eq)]
120enum Source {
121    Script,
122    Package,
123}
124
125/// The rules a script and a package share, in the order they report.
126fn run_rules(
127    ta: &TypedAst,
128    declarations: &TypeDeclarations<'_>,
129    source: Source,
130    diags: &mut Vec<Diagnostic>,
131) -> Result<(), CompilerFailure> {
132    missing_return::run(ta, declarations, diags)?;
133    unreachable::run(ta, declarations, diags)?;
134    return_outside_function::run(ta, diags)?;
135    fallthrough::run(ta, declarations, diags)?;
136    definite_assignment::run(ta, diags)?;
137    super_call::run(ta, diags)?;
138    if source == Source::Script {
139        // Inference reports a package that declares `main`.
140        main_required::run(ta, diags);
141    }
142    doc_consistency::run(ta, diags)?;
143    capability_consistency::run(ta, declarations, diags)
144}
145
146#[cfg(test)]
147mod test_util {
148    use std::collections::BTreeMap;
149
150    use super::check_script;
151    use crate::{
152        Asi, Ast, Diagnostic, FileId, ModulePath, PackageDeclaration, Sources, Token, TokenKind,
153        TypedAst, infer, infer_package, lower_patterns, parse,
154    };
155
156    /// Most rule tests aren't about `main` — prefix the source with a no-op
157    /// `main` so the missing-main diagnostic doesn't pollute their counts.
158    pub fn run(source: &str) -> Vec<Diagnostic> {
159        let with_main = format!("function main(): void {{ }}\n{source}");
160        pipeline(&with_main)
161    }
162
163    pub fn run_raw(source: &str) -> Vec<Diagnostic> {
164        pipeline(source)
165    }
166
167    /// Each diagnostic of [`run_raw`] as its message and 1-based line, so a
168    /// test can tell apart several reports that share a message.
169    pub fn run_lines(source: &str) -> Vec<(String, usize)> {
170        run_raw(source)
171            .into_iter()
172            .map(|diag| {
173                let line = source[..diag.span.start as usize].lines().count().max(1);
174                (diag.message, line)
175            })
176            .collect()
177    }
178
179    /// Infers a script against the runtime and the standard library, without
180    /// checking it.
181    pub fn infer_script(source: &str) -> (TypedAst, Vec<Diagnostic>) {
182        infer_script_with(source, &[])
183    }
184
185    /// [`infer_script`] with `packages` to import from as well.
186    pub fn infer_script_with(
187        source: &str,
188        packages: &[PackageDeclaration],
189    ) -> (TypedAst, Vec<Diagnostic>) {
190        let ast = parse_source(source, FileId(0));
191        let dependencies = runtime_declarations();
192        let dependencies: Vec<_> = dependencies.iter().chain(packages).collect();
193        infer(source, "main", &ast, &dependencies)
194    }
195
196    /// Infers and checks the package `modules` make up, rooted at `lib`.
197    pub fn run_package(
198        name: &str,
199        modules: &[(&str, &str)],
200        dependencies: &[PackageDeclaration],
201    ) -> (TypedAst, PackageDeclaration, Vec<Diagnostic>) {
202        let mut sources = Sources::new();
203        let parsed: Vec<(ModulePath, FileId, Ast)> = modules
204            .iter()
205            .map(|(module, source)| {
206                let file = sources.add((*module).to_string(), *source).unwrap();
207                let ast = lower_patterns(parse_source(source, file)).unwrap();
208                (ModulePath::from(*module), file, ast)
209            })
210            .collect();
211        let external: BTreeMap<String, PackageDeclaration> = runtime_declarations()
212            .into_iter()
213            .chain(dependencies.iter().cloned())
214            .map(|declaration| (declaration.package_name.clone(), declaration))
215            .collect();
216        infer_package(
217            name,
218            ModulePath::from("lib"),
219            parsed
220                .iter()
221                .map(|(module, file, ast)| (module.clone(), *file, ast))
222                .collect(),
223            &sources,
224            external,
225            BTreeMap::new(),
226        )
227    }
228
229    fn pipeline(source: &str) -> Vec<Diagnostic> {
230        let ast = parse_source(source, FileId(0));
231        let (prelude_defs, host_defs, _) =
232            crate::runtime::prelude::cached_runtime_package_declarations();
233        let mut packages = Vec::with_capacity(prelude_defs.len() + host_defs.len());
234        packages.extend(prelude_defs.iter());
235        packages.extend(host_defs.iter());
236        let (ta, mut diags) = infer(source, "main", &ast, &packages);
237        diags.extend(check_script(&ta, &packages).unwrap());
238        diags
239    }
240
241    fn parse_source(source: &str, file: FileId) -> Ast {
242        let mut asi = Asi::new(source, file);
243        let mut tokens: Vec<Token> = Vec::new();
244        loop {
245            let tok = asi.next_token();
246            let is_eof = matches!(tok.kind, TokenKind::Eof);
247            tokens.push(tok);
248            if is_eof {
249                break;
250            }
251        }
252        let lex_diags = asi.into_diagnostics();
253        assert!(
254            lex_diags.is_empty(),
255            "unexpected lexer diags: {lex_diags:?}"
256        );
257        let (ast, parse_diags) = parse(source, tokens, file);
258        assert!(
259            parse_diags.is_empty(),
260            "unexpected parser diags: {parse_diags:?}"
261        );
262        ast
263    }
264
265    fn runtime_declarations() -> Vec<PackageDeclaration> {
266        let (prelude_defs, host_defs, _) =
267            crate::runtime::prelude::cached_runtime_package_declarations();
268        prelude_defs
269            .iter()
270            .chain(host_defs)
271            .cloned()
272            .chain(crate::stdlib::stdlib_package_declarations())
273            .collect()
274    }
275}