Skip to main content

i_slint_compiler/passes/
resolve_native_classes.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
4//! After inlining and moving declarations, all Element::base_type should be Type::BuiltinElement. This pass resolves them
5//! to NativeClass and picking a variant that only contains the used properties.
6//! The default values of the properties the variant doesn't have are dropped along with them.
7
8use smol_str::SmolStr;
9use std::collections::HashSet;
10use std::sync::Arc;
11
12use crate::expression_tree::{BindingExpression, Expression};
13use crate::langtype::{BuiltinElement, BuiltinPropertyDefault, ElementType, NativeClass};
14use crate::object_tree::{Component, recurse_elem_including_sub_components};
15
16pub fn resolve_native_classes(component: &Component) {
17    recurse_elem_including_sub_components(component, &(), &mut |elem, _| {
18        let (new_native_class, unused_defaults) = {
19            let elem = elem.borrow();
20
21            let base_type = match &elem.base_type {
22                ElementType::Component(_) => {
23                    // recurse_elem_including_sub_components will recurse into it
24                    return;
25                }
26                ElementType::Builtin(b) => b,
27                ElementType::Native(_) => {
28                    // already native
29                    return;
30                }
31                ElementType::Interface | ElementType::Global | ElementType::Error => {
32                    panic!("This should not happen")
33                }
34            };
35
36            let defaults: Vec<&SmolStr> = elem
37                .bindings_including_synthetic()
38                .filter(|(name, binding)| is_default_value(base_type, name, &binding.borrow()))
39                .map(|(name, _)| name)
40                .collect();
41
42            let analysis = elem.property_analysis.borrow();
43            let native_properties_used: HashSet<_> = elem
44                .bindings_including_synthetic()
45                .map(|(k, _)| k)
46                .filter(|k| !defaults.contains(k))
47                .chain(analysis.iter().filter(|(_, v)| v.is_used()).map(|(k, _)| k))
48                .filter(|k| {
49                    !elem.property_declarations.contains_key(*k)
50                        && base_type.as_ref().properties.contains_key(*k)
51                })
52                .collect();
53
54            let new_native_class = select_minimal_class_based_on_property_usage(
55                &base_type.native_class,
56                native_properties_used.into_iter(),
57            );
58
59            // A referenced property keeps its default: the reference materializes it in the
60            // enclosing component, which is how a lowered layout still reads its own alignment.
61            let unused_defaults: Vec<SmolStr> = defaults
62                .into_iter()
63                .filter(|name| {
64                    new_native_class.lookup_property(name).is_none()
65                        && !elem.named_references.is_referenced(name)
66                })
67                .cloned()
68                .collect();
69
70            (new_native_class, unused_defaults)
71        };
72
73        let mut elem = elem.borrow_mut();
74        for name in unused_defaults {
75            elem.take_binding_including_synthetic(&name);
76        }
77        elem.base_type = ElementType::Native(new_native_class);
78    })
79}
80
81/// Whether this binding just sets the property to the default value of the builtin
82/// element declaration, so that nothing changes if it goes away.
83fn is_default_value(base_type: &BuiltinElement, name: &str, binding: &BindingExpression) -> bool {
84    let Some(BuiltinPropertyDefault::Expr(default)) =
85        base_type.properties.get(name).map(|p| &p.default_value)
86    else {
87        return false;
88    };
89    binding.animation.is_none()
90        && binding.two_way_bindings.is_empty()
91        && same_literal(binding.value_expression(), &default.to_expression())
92}
93
94/// Anything that isn't a literal compares as different, so a computed binding counts as a use.
95fn same_literal(a: &Expression, b: &Expression) -> bool {
96    match (a, b) {
97        (Expression::NumberLiteral(a, a_unit), Expression::NumberLiteral(b, b_unit)) => {
98            a == b && a_unit == b_unit
99        }
100        (Expression::BoolLiteral(a), Expression::BoolLiteral(b)) => a == b,
101        (Expression::StringLiteral(a), Expression::StringLiteral(b)) => a == b,
102        (Expression::EnumerationValue(a), Expression::EnumerationValue(b)) => a == b,
103        // Colors and other converted literals arrive wrapped in a cast.
104        (Expression::Cast { from: a, to: a_type }, Expression::Cast { from: b, to: b_type }) => {
105            a_type == b_type && same_literal(a, b)
106        }
107        _ => false,
108    }
109}
110
111fn lookup_property_distance(mut class: Arc<NativeClass>, name: &str) -> (usize, Arc<NativeClass>) {
112    let mut distance = 0;
113    loop {
114        if class.properties.contains_key(name)
115            || (class.parent.is_none() && ["x", "y", "width", "height"].contains(&name))
116        {
117            return (distance, class);
118        }
119        distance += 1;
120        class = class.parent.as_ref().unwrap().clone();
121    }
122}
123
124fn select_minimal_class_based_on_property_usage<'a>(
125    class: &Arc<NativeClass>,
126    properties_used: impl Iterator<Item = &'a SmolStr>,
127) -> Arc<NativeClass> {
128    let mut minimal_class = class.clone();
129    while let Some(class) = minimal_class.parent.clone() {
130        minimal_class = class;
131    }
132    let (_min_distance, minimal_class) = properties_used.fold(
133        (usize::MAX, minimal_class),
134        |(current_distance, current_class), prop_name| {
135            let (prop_distance, prop_class) = lookup_property_distance(class.clone(), prop_name);
136
137            if prop_distance < current_distance {
138                (prop_distance, prop_class)
139            } else {
140                (current_distance, current_class)
141            }
142        },
143    );
144    minimal_class
145}
146
147#[test]
148fn test_select_minimal_class_based_on_property_usage() {
149    use crate::langtype::{BuiltinPropertyInfo, Type};
150    use smol_str::ToSmolStr;
151    let first = Arc::new(NativeClass::new_with_properties(
152        "first_class",
153        [("first_prop".to_smolstr(), BuiltinPropertyInfo::new(Type::Int32))].iter().cloned(),
154    ));
155
156    let mut second = NativeClass::new_with_properties(
157        "second_class",
158        [("second_prop".to_smolstr(), BuiltinPropertyInfo::new(Type::Int32))].iter().cloned(),
159    );
160    second.parent = Some(first.clone());
161    let second = Arc::new(second);
162
163    let reduce_to_first =
164        select_minimal_class_based_on_property_usage(&second, ["first_prop".to_smolstr()].iter());
165
166    assert_eq!(reduce_to_first.class_name, first.class_name);
167
168    let reduce_to_second =
169        select_minimal_class_based_on_property_usage(&second, ["second_prop".to_smolstr()].iter());
170
171    assert_eq!(reduce_to_second.class_name, second.class_name);
172
173    let reduce_to_second = select_minimal_class_based_on_property_usage(
174        &second,
175        ["first_prop".to_smolstr(), "second_prop".to_smolstr()].iter(),
176    );
177
178    assert_eq!(reduce_to_second.class_name, second.class_name);
179}
180
181#[test]
182fn builtin_defaults_are_comparable() {
183    let tr = crate::typeregister::TypeRegister::builtin();
184    let tr = tr.borrow();
185    for (name, element) in tr.all_elements() {
186        let ElementType::Builtin(element) = element else { continue };
187        for (property, info) in &element.properties {
188            if let BuiltinPropertyDefault::Expr(default) = &info.default_value {
189                let default = default.to_expression();
190                assert!(
191                    same_literal(&default, &default),
192                    "the default of {name}::{property} is a shape same_literal doesn't compare, \
193                     so the property always counts as used: {default:?}"
194                );
195            }
196        }
197    }
198}
199
200#[test]
201fn select_minimal_class() {
202    use smol_str::ToSmolStr;
203    let tr = crate::typeregister::TypeRegister::builtin();
204    let tr = tr.borrow();
205    let rect = tr.lookup_element("Rectangle").unwrap();
206    let rect = rect.as_builtin();
207    assert_eq!(
208        select_minimal_class_based_on_property_usage(
209            &rect.native_class,
210            ["x".to_smolstr(), "width".to_smolstr()].iter()
211        )
212        .class_name,
213        "Empty",
214    );
215    assert_eq!(
216        select_minimal_class_based_on_property_usage(&rect.native_class, [].iter()).class_name,
217        "Empty",
218    );
219    assert_eq!(
220        select_minimal_class_based_on_property_usage(
221            &rect.native_class,
222            ["border-width".to_smolstr()].iter()
223        )
224        .class_name,
225        "BasicBorderRectangle",
226    );
227    assert_eq!(
228        select_minimal_class_based_on_property_usage(
229            &rect.native_class,
230            ["border-width".to_smolstr(), "x".to_smolstr()].iter()
231        )
232        .class_name,
233        "BasicBorderRectangle",
234    );
235    assert_eq!(
236        select_minimal_class_based_on_property_usage(
237            &rect.native_class,
238            ["border-top-left-radius".to_smolstr(), "x".to_smolstr()].iter()
239        )
240        .class_name,
241        "BorderRectangle",
242    );
243}