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;
16pub(crate) mod 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, PackageDeclaration, Type, TypedAst, tree_height};
25
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, true, &mut diags).map_err(|fatal| {
49        CompileError {
50            diagnostics: diags.clone(),
51            fatal: Some(fatal),
52        }
53    })?;
54    Ok(diags)
55}
56
57/// Checks only the rules specific to Submilli on a script typed by either front end.
58pub fn check_script_submilli(
59    ta: &TypedAst,
60    dependencies: &[&PackageDeclaration],
61) -> Result<Vec<Diagnostic>, CompileError> {
62    tree_height::check_typed(ta, CompilerStage::Infer)?;
63    super::infer::check_declaration_types(dependencies.iter().copied())?;
64    let declarations = TypeDeclarations::for_script(ta, dependencies);
65    let mut diags = Vec::new();
66    run_rules(ta, &declarations, Source::Script, false, &mut diags).map_err(|fatal| {
67        CompileError {
68            diagnostics: diags.clone(),
69            fatal: Some(fatal),
70        }
71    })?;
72    Ok(diags)
73}
74
75/// The type a `@capability` binding path reads from a parameter of type `ty`
76/// in `package`, resolved as the capability-consistency rule resolves it.
77/// `None` when a segment names no field or reaches an unknown declaration.
78/// `ty` must satisfy [`crate::type_size::check`], and all declarations must pass
79/// [`PackageDeclaration::check_type_limits`] since their last mutation; `None` is
80/// not a validation failure.
81pub fn capability_binding_type(
82    package: &PackageDeclaration,
83    dependencies: &[&PackageDeclaration],
84    ty: &Type,
85    path: &[String],
86) -> Option<Type> {
87    let declarations = TypeDeclarations::for_package(package, dependencies);
88    match capability_consistency::binding_target(&declarations, ty, path) {
89        capability_consistency::BindingTarget::Found(ty) => Some(ty),
90        capability_consistency::BindingTarget::Missing(_)
91        | capability_consistency::BindingTarget::Unresolved => None,
92    }
93}
94
95/// Checks a package. `dependencies` are the packages it was inferred against,
96/// the ones it cannot import from included.
97pub fn check_package(
98    typed_ast: &TypedAst,
99    package: &PackageDeclaration,
100    dependencies: &[&PackageDeclaration],
101) -> Result<Vec<Diagnostic>, CompileError> {
102    check_package_rules(
103        typed_ast,
104        package,
105        dependencies,
106        true,
107        &crate::type_size::TypeLimits::default(),
108    )
109}
110
111/// Checks only the rules specific to Submilli on a package typed by either front end.
112pub fn check_package_submilli(
113    typed_ast: &TypedAst,
114    package: &PackageDeclaration,
115    dependencies: &[&PackageDeclaration],
116) -> Result<Vec<Diagnostic>, CompileError> {
117    check_package_rules(
118        typed_ast,
119        package,
120        dependencies,
121        false,
122        &crate::type_size::TypeLimits::default(),
123    )
124}
125
126pub(in crate::typechecker) fn check_package_rules(
127    typed_ast: &TypedAst,
128    package: &PackageDeclaration,
129    dependencies: &[&PackageDeclaration],
130    include_typescript: bool,
131    limits: &crate::type_size::TypeLimits,
132) -> Result<Vec<Diagnostic>, CompileError> {
133    tree_height::check_typed(typed_ast, CompilerStage::Infer)?;
134    let root_module = typed_ast.package_root_module.as_ref().ok_or_else(|| {
135        CompileError::from(super::invariant_failure(
136            "package TypedAst lacks its root module path",
137        ))
138    })?;
139    super::infer::check_declaration_types(
140        std::iter::once(package).chain(dependencies.iter().copied()),
141    )?;
142    let mut registry = super::infer::type_registry::TypeRegistry::new();
143    for declaration in std::iter::once(package).chain(dependencies.iter().copied()) {
144        for symbol in declaration
145            .runtime_types
146            .values()
147            .chain(declaration.types.values())
148        {
149            registry.insert_borrowed(symbol);
150        }
151    }
152    let namespace = super::infer::type_namespace::TypeNamespace::new();
153    let constraints = Default::default();
154    let resolver = super::infer::assignable::TypeResolver {
155        types: &namespace,
156        registry: &registry,
157        limits,
158        strict_subtype: false,
159        generic_constraints: &constraints,
160    };
161    let mut diags = Vec::new();
162    finalizers::run(typed_ast, package, resolver, &mut diags).map_err(|fatal| CompileError {
163        diagnostics: diags.clone(),
164        fatal: Some(fatal),
165    })?;
166    let exports = &typed_ast.exports;
167    exported_signature_types::run_modules(
168        &package.package_name,
169        "module",
170        typed_ast,
171        package,
172        root_module,
173        &mut diags,
174    );
175    exported_signature_types::run(
176        &package.package_name,
177        "package",
178        package,
179        exports,
180        &mut diags,
181    );
182    exported_docs::run(typed_ast, package, exports, &mut diags);
183    let declarations = TypeDeclarations::for_package(package, dependencies);
184    run_rules(
185        typed_ast,
186        &declarations,
187        Source::Package,
188        include_typescript,
189        &mut diags,
190    )
191    .and_then(|()| check_discipline::run(typed_ast, &mut diags))
192    .map_err(|fatal| CompileError {
193        diagnostics: diags.clone(),
194        fatal: Some(fatal),
195    })?;
196    Ok(diags)
197}
198
199#[derive(Clone, Copy, PartialEq, Eq)]
200enum Source {
201    Script,
202    Package,
203}
204
205/// The rules a script and a package share, in the order they report.
206fn run_rules(
207    ta: &TypedAst,
208    declarations: &TypeDeclarations<'_>,
209    source: Source,
210    include_typescript: bool,
211    diags: &mut Vec<Diagnostic>,
212) -> Result<(), CompilerFailure> {
213    if include_typescript {
214        missing_return::run(ta, declarations, diags)?;
215        unreachable::run(ta, declarations, diags)?;
216        return_outside_function::run(ta, diags)?;
217        fallthrough::run(ta, declarations, diags)?;
218        definite_assignment::run(ta, diags)?;
219    }
220    super_call::run(ta, diags)?;
221    if source == Source::Script {
222        // Inference reports a package that declares `main`.
223        main_required::run(ta, diags);
224    }
225    doc_consistency::run(ta, diags)?;
226    capability_consistency::run(ta, declarations, diags)
227}
228
229#[cfg(test)]
230pub(crate) mod test_util {
231    use std::collections::BTreeMap;
232
233    use super::check_script;
234    use crate::{
235        Asi, Ast, Diagnostic, FileId, ModulePath, PackageDeclaration, Sources, Token, TokenKind,
236        TypedAst, infer, infer_package, lower_patterns, parse,
237    };
238
239    /// Most rule tests aren't about `main` — prefix the source with a no-op
240    /// `main` so the missing-main diagnostic doesn't pollute their counts.
241    pub fn run(source: &str) -> Vec<Diagnostic> {
242        let with_main = format!("function main(): void {{ }}\n{source}");
243        pipeline(&with_main)
244    }
245
246    pub fn run_raw(source: &str) -> Vec<Diagnostic> {
247        pipeline(source)
248    }
249
250    /// Every diagnostic the whole compile pipeline reports for `source`. Unlike
251    /// [`run_raw`], parse errors are included and later phases still run.
252    pub fn compile_messages(source: &str) -> Vec<String> {
253        crate::typecheck(source, FileId(0))
254            .err()
255            .unwrap_or_default()
256            .into_iter()
257            .map(|diag| diag.message)
258            .collect()
259    }
260
261    /// Each diagnostic of [`run_raw`] as its message and 1-based line, so a
262    /// test can tell apart several reports that share a message.
263    pub fn run_lines(source: &str) -> Vec<(String, usize)> {
264        run_raw(source)
265            .into_iter()
266            .map(|diag| {
267                let line = source[..diag.span.start as usize].lines().count().max(1);
268                (diag.message, line)
269            })
270            .collect()
271    }
272
273    /// Infers a script against the runtime and the standard library, without
274    /// checking it.
275    pub fn infer_script(source: &str) -> (TypedAst, Vec<Diagnostic>) {
276        infer_script_with(source, &[])
277    }
278
279    /// [`infer_script`] with `packages` to import from as well.
280    pub fn infer_script_with(
281        source: &str,
282        packages: &[PackageDeclaration],
283    ) -> (TypedAst, Vec<Diagnostic>) {
284        let ast = parse_source(source, FileId(0));
285        let dependencies = runtime_declarations();
286        let dependencies: Vec<_> = dependencies.iter().chain(packages).collect();
287        infer(source, "main", &ast, &dependencies)
288    }
289
290    /// Infers and checks the package `modules` make up, rooted at `lib`.
291    pub fn run_package(
292        name: &str,
293        modules: &[(&str, &str)],
294        dependencies: &[PackageDeclaration],
295    ) -> (TypedAst, PackageDeclaration, Vec<Diagnostic>) {
296        let mut sources = Sources::new();
297        let parsed: Vec<(ModulePath, FileId, Ast)> = modules
298            .iter()
299            .map(|(module, source)| {
300                let file = sources.add((*module).to_string(), *source).unwrap();
301                let ast = lower_patterns(parse_source(source, file)).unwrap();
302                (ModulePath::from(*module), file, ast)
303            })
304            .collect();
305        let external: BTreeMap<String, PackageDeclaration> = runtime_declarations()
306            .into_iter()
307            .chain(dependencies.iter().cloned())
308            .map(|declaration| (declaration.package_name.clone(), declaration))
309            .collect();
310        infer_package(
311            name,
312            ModulePath::from("lib"),
313            parsed
314                .iter()
315                .map(|(module, file, ast)| (module.clone(), *file, ast))
316                .collect(),
317            &sources,
318            external,
319            BTreeMap::new(),
320        )
321    }
322
323    fn pipeline(source: &str) -> Vec<Diagnostic> {
324        let ast = parse_source(source, FileId(0));
325        let (prelude_defs, host_defs, _) =
326            crate::runtime::prelude::cached_runtime_package_declarations();
327        let mut packages = Vec::with_capacity(prelude_defs.len() + host_defs.len());
328        packages.extend(prelude_defs.iter());
329        packages.extend(host_defs.iter());
330        let (ta, mut diags) = infer(source, "main", &ast, &packages);
331        diags.extend(check_script(&ta, &packages).unwrap());
332        diags
333    }
334
335    fn parse_source(source: &str, file: FileId) -> Ast {
336        let mut asi = Asi::new(source, file);
337        let mut tokens: Vec<Token> = Vec::new();
338        loop {
339            let tok = asi.next_token();
340            let is_eof = matches!(tok.kind, TokenKind::Eof);
341            tokens.push(tok);
342            if is_eof {
343                break;
344            }
345        }
346        let lex_diags = asi.into_diagnostics();
347        assert!(
348            lex_diags.is_empty(),
349            "unexpected lexer diags: {lex_diags:?}"
350        );
351        let (ast, parse_diags) = parse(source, tokens, file);
352        assert!(
353            parse_diags.is_empty(),
354            "unexpected parser diags: {parse_diags:?}"
355        );
356        ast
357    }
358
359    fn runtime_declarations() -> Vec<PackageDeclaration> {
360        let (prelude_defs, host_defs, _) =
361            crate::runtime::prelude::cached_runtime_package_declarations();
362        prelude_defs
363            .iter()
364            .chain(host_defs)
365            .cloned()
366            .chain(crate::stdlib::stdlib_package_declarations())
367            .collect()
368    }
369}
370
371#[cfg(test)]
372mod public_entry_tests {
373    use super::test_util::{infer_script, run_package};
374    use super::{check_package, check_package_submilli, check_script, check_script_submilli};
375
376    #[test]
377    fn submilli_script_check_omits_typescript_rules() {
378        let (typed_ast, inference) =
379            infer_script("function main(): void { return; const value = 1; }");
380        assert!(
381            !inference
382                .iter()
383                .any(|diagnostic| diagnostic.severity == crate::Severity::Error),
384            "{inference:?}"
385        );
386        let full = check_script(&typed_ast, &[]).unwrap();
387        assert!(
388            full.iter()
389                .any(|diagnostic| diagnostic.message.contains("unreachable"))
390        );
391        assert!(check_script_submilli(&typed_ast, &[]).unwrap().is_empty());
392        let (without_main, _) = infer_script("function helper(): void {}");
393        assert!(
394            check_script_submilli(&without_main, &[])
395                .unwrap()
396                .iter()
397                .any(|diagnostic| diagnostic.message.contains("main"))
398        );
399    }
400
401    #[test]
402    fn package_entry_needs_only_tree_and_declarations() {
403        let (typed_ast, package, inference) = run_package(
404            "@test/public-rules",
405            &[
406                ("lib", "export function entry(): void {}"),
407                ("other", "export function helper(): void {}"),
408            ],
409            &[],
410        );
411        assert!(
412            !inference
413                .iter()
414                .any(|diagnostic| diagnostic.severity == crate::Severity::Error),
415            "{inference:?}"
416        );
417        assert_eq!(
418            check_package(&typed_ast, &package, &[]).unwrap(),
419            check_package_submilli(&typed_ast, &package, &[]).unwrap()
420        );
421        assert_eq!(
422            typed_ast.package_root_module,
423            Some(crate::ModulePath::from("lib"))
424        );
425        let mut without_root = typed_ast.clone();
426        without_root.package_root_module = None;
427        assert!(check_package(&without_root, &package, &[]).is_err());
428    }
429
430    #[test]
431    fn package_entry_checks_private_module_signature_types() {
432        let (typed_ast, package, _) = run_package(
433            "@test/private-type",
434            &[
435                ("lib", "export function entry(): void {}"),
436                (
437                    "other",
438                    "interface Hidden { value: string } export function expose(value: Hidden): void {}",
439                ),
440            ],
441            &[],
442        );
443        let diagnostics = check_package_submilli(&typed_ast, &package, &[]).unwrap();
444        assert!(
445            diagnostics
446                .iter()
447                .any(|diagnostic| diagnostic.message.contains("private type `Hidden`")),
448            "{diagnostics:?}"
449        );
450    }
451}