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