Skip to main content

i_slint_compiler/passes/
purity_check.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::collections::HashSet;
5
6use crate::diagnostics::{BuildDiagnostics, DiagnosticLevel};
7use crate::expression_tree::{Callable, Expression, NamedReference};
8use crate::langtype::PropertyLookupMode;
9
10/// Check that pure expression only call pure functions
11pub fn purity_check(doc: &crate::object_tree::Document, diag: &mut BuildDiagnostics) {
12    for component in &doc.inner_components {
13        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
14            component,
15            &(),
16            &mut |elem, &()| {
17                let level = match elem.borrow().is_legacy_syntax {
18                    true => DiagnosticLevel::Warning,
19                    false => DiagnosticLevel::Error,
20                };
21                crate::object_tree::visit_element_expressions(elem, |expr, name, _| {
22                    if let Some(name) = name {
23                        let lookup =
24                            elem.borrow().lookup_property(name, PropertyLookupMode::InternalName);
25                        if lookup.declared_pure.unwrap_or(false)
26                            || lookup.property_type.is_property_type()
27                        {
28                            ensure_pure(expr, Some((diag, level)), &mut Default::default());
29                        }
30                    } else {
31                        // model expression must be pure
32                        ensure_pure(expr, Some((diag, level)), &mut Default::default());
33                    };
34                })
35            },
36        )
37    }
38}
39
40/// Whether evaluating `expr` has no side effect: it assigns no property and calls nothing impure.
41/// A `pure` declaration is taken at face value, which the legacy syntax only warns about.
42pub(super) fn is_pure(expr: &Expression) -> bool {
43    ensure_pure(expr, None, &mut Default::default())
44}
45
46fn ensure_pure(
47    expr: &Expression,
48    mut diag: Option<(&mut BuildDiagnostics, DiagnosticLevel)>,
49    recursion_test: &mut HashSet<NamedReference>,
50) -> bool {
51    let mut r = true;
52    expr.visit_recursive(&mut |e| match e {
53        Expression::FunctionCall { function: Callable::Callback(nr), source_location, .. }
54            if !nr
55                .element()
56                .borrow()
57                .lookup_property(nr.name(), PropertyLookupMode::InternalName)
58                .declared_pure
59                .unwrap_or(false) =>
60        {
61            if let Some((diag, level)) = diag.as_mut() {
62                diag.push_diagnostic(
63                    format!("Call of impure callback '{}'", nr.declared_name()),
64                    source_location,
65                    *level,
66                );
67            }
68            r = false;
69        }
70        Expression::FunctionCall { function: Callable::Function(nr), source_location, .. }
71            if !function_is_pure(nr, recursion_test) =>
72        {
73            if let Some((diag, level)) = diag.as_mut() {
74                diag.push_diagnostic(
75                    format!("Call of impure function '{}'", nr.declared_name()),
76                    source_location,
77                    *level,
78                );
79            }
80            r = false;
81        }
82        Expression::FunctionCall { function: Callable::Builtin(func), source_location, .. }
83            if !func.is_pure() =>
84        {
85            if let Some((diag, level)) = diag.as_mut() {
86                diag.push_diagnostic("Call of impure function".into(), source_location, *level);
87            }
88            r = false;
89        }
90        Expression::SelfAssignment { node, .. } => {
91            if let Some((diag, level)) = diag.as_mut() {
92                diag.push_diagnostic("Assignment in a pure context".into(), node, *level);
93            }
94            r = false;
95        }
96        _ => (),
97    });
98    r
99}
100
101/// Whether calling the function `nr` is pure.
102/// A private function carries no declaration, so it is judged by its body.
103fn function_is_pure(nr: &NamedReference, recursion_test: &mut HashSet<NamedReference>) -> bool {
104    let element = nr.element();
105    let element = element.borrow();
106    if let Some(declared) =
107        element.lookup_property(nr.name(), PropertyLookupMode::InternalName).declared_pure
108    {
109        return declared;
110    }
111    // A function already under inspection is a cycle, reported as a binding loop elsewhere.
112    if !recursion_test.insert(nr.clone()) {
113        return true;
114    }
115    match element.binding_cell_including_synthetic(nr.name()).map(|body| body.try_borrow()) {
116        Some(Ok(body)) => ensure_pure(&body.expression, None, recursion_test),
117        // The expression visitor holds a mutable borrow on the body it is visiting, and that
118        // function isn't in `recursion_test`. A failed borrow is a call back into it: a cycle too.
119        Some(Err(_)) => true,
120        // Only reached for a lookup that already failed with an error.
121        None => true,
122    }
123}