Skip to main content

i_slint_compiler/llr/optim_passes/
remove_unused.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 crate::llr::*;
5use typed_index_collections::TiVec;
6
7struct Mapping {
8    prop_mapping: TiVec<PropertyIdx, Option<PropertyIdx>>,
9    callback_mapping: TiVec<CallbackIdx, Option<CallbackIdx>>,
10    function_mapping: TiVec<FunctionIdx, Option<FunctionIdx>>,
11}
12
13impl Mapping {
14    fn keep(&self, member: &LocalMemberIndex) -> bool {
15        match member {
16            LocalMemberIndex::Property(p) => self.prop_mapping[*p].is_some(),
17            LocalMemberIndex::Callback(c) => self.callback_mapping[*c].is_some(),
18            LocalMemberIndex::Function(f) => self.function_mapping[*f].is_some(),
19            LocalMemberIndex::Native { .. } | LocalMemberIndex::Timer(_) => true,
20        }
21    }
22}
23
24type ScMappings = TiVec<SubComponentIdx, Mapping>;
25type GlobMappings = TiVec<GlobalIdx, Mapping>;
26
27pub fn remove_unused(root: &mut CompilationUnit) {
28    struct RemoveUnusedMappings {
29        sc_mappings: ScMappings,
30        glob_mappings: GlobMappings,
31    }
32    let mappings = RemoveUnusedMappings {
33        sc_mappings: root
34            .sub_components
35            .iter_mut()
36            .map(|sc| create_mapping(&mut sc.properties, &mut sc.callbacks, &mut sc.functions))
37            .collect(),
38        glob_mappings: root
39            .globals
40            .iter_mut()
41            .map(|g| {
42                clean_vec(&mut g.const_properties, &g.properties);
43                clean_vec(&mut g.prop_analysis, &g.properties);
44                create_mapping(&mut g.properties, &mut g.callbacks, &mut g.functions)
45            })
46            .collect(),
47    };
48
49    let state = visitor::VisitorState::new(root);
50
51    for (idx, sc) in root.sub_components.iter_mut_enumerated() {
52        let keep = |refer: &MemberReference| match refer {
53            MemberReference::Relative { parent_level, local_reference } => {
54                assert_eq!(*parent_level, 0);
55                let idx = state.follow_sub_components(idx, &local_reference.sub_component_path);
56                mappings.sc_mappings[idx].keep(&local_reference.reference)
57            }
58            MemberReference::Global { global_index, member } => {
59                mappings.glob_mappings[*global_index].keep(member)
60            }
61        };
62
63        let mut property_init_mapping = Vec::new();
64        let mut i = 0;
65        sc.property_init.retain(|(x, v)| {
66            if keep(x) && v.use_count.get() > 0 {
67                property_init_mapping.push(Some(i));
68                i += 1;
69                true
70            } else {
71                property_init_mapping.push(None);
72                false
73            }
74        });
75        sc.change_callbacks.retain(|(x, _)| keep(x));
76        sc.const_properties.retain(|x| {
77            let idx = state.follow_sub_components(idx, &x.sub_component_path);
78            mappings.sc_mappings[idx].keep(&x.reference)
79        });
80        sc.prop_analysis.retain(|x, v| {
81            v.property_init = v.property_init.and_then(|x| property_init_mapping[x]);
82            keep(x)
83        });
84        sc.animations.retain(|x, _| keep(&x.clone().into()));
85    }
86    for (idx, g) in root.globals.iter_mut_enumerated() {
87        g.init_values.retain(|x, _| mappings.glob_mappings[idx].keep(x));
88    }
89
90    macro_rules! remap_index {
91        ($method:ident, $idx:ty, $field:ident) => {
92            fn $method(
93                &mut self,
94                p: &mut $idx,
95                scope: &EvaluationScope,
96                _state: &visitor::VisitorState,
97            ) {
98                *p = match scope {
99                    EvaluationScope::SubComponent(sub_component_idx, _) => {
100                        self.sc_mappings[*sub_component_idx].$field[*p]
101                    }
102                    EvaluationScope::Global(global_idx) => {
103                        self.glob_mappings[*global_idx].$field[*p]
104                    }
105                    EvaluationScope::Const => {
106                        panic!("member reference in a constant expression")
107                    }
108                }
109                .unwrap();
110            }
111        };
112    }
113    impl visitor::Visitor for &RemoveUnusedMappings {
114        // All three remap an index through the mapping of the enclosing scope. If one of the
115        // unwraps fails, count_property_use() forgot to visit something, so a member that is
116        // still referenced was removed.
117        remap_index!(visit_property_idx, PropertyIdx, prop_mapping);
118        remap_index!(visit_callback_idx, CallbackIdx, callback_mapping);
119        remap_index!(visit_function_idx, FunctionIdx, function_mapping);
120    }
121    let mut visitor = &mappings;
122    visitor::visit_compilation_unit(root, &state, &mut visitor);
123}
124
125fn create_mapping(
126    properties: &mut TiVec<PropertyIdx, Property>,
127    callbacks: &mut TiVec<CallbackIdx, Callback>,
128    functions: &mut TiVec<FunctionIdx, Function>,
129) -> Mapping {
130    Mapping {
131        prop_mapping: create_vec_mapping(properties, |p| p.use_count.get() > 0),
132        callback_mapping: create_vec_mapping(callbacks, |c| c.use_count.get() > 0),
133        function_mapping: create_vec_mapping(functions, |f| f.use_count.get() > 0),
134    }
135}
136
137fn create_vec_mapping<Idx: From<usize>, T>(
138    vec: &mut TiVec<Idx, T>,
139    mut retain: impl FnMut(&T) -> bool,
140) -> TiVec<Idx, Option<Idx>> {
141    let mut map = TiVec::with_capacity(vec.len());
142    let mut i = 0;
143    vec.retain(|t| {
144        if retain(t) {
145            map.push(Some(Idx::from(i)));
146            i += 1;
147            true
148        } else {
149            map.push(None);
150            false
151        }
152    });
153    map
154}
155
156fn clean_vec<T>(vec: &mut TiVec<PropertyIdx, T>, properties: &TiVec<PropertyIdx, Property>) {
157    let mut idx = 0;
158    vec.retain(|_| {
159        idx += 1;
160        properties[PropertyIdx::from(idx - 1)].use_count.get() >= 1
161    });
162}
163
164mod visitor {
165
166    use super::*;
167
168    pub trait Visitor {
169        fn visit_property_idx(
170            &mut self,
171            _p: &mut PropertyIdx,
172            _scope: &EvaluationScope,
173            _state: &VisitorState,
174        ) {
175        }
176        fn visit_function_idx(
177            &mut self,
178            _p: &mut FunctionIdx,
179            _scope: &EvaluationScope,
180            _state: &VisitorState,
181        ) {
182        }
183
184        fn visit_callback_idx(
185            &mut self,
186            _p: &mut CallbackIdx,
187            _scope: &EvaluationScope,
188            _state: &VisitorState,
189        ) {
190        }
191    }
192
193    pub struct VisitorState {
194        /// Copy of SubComponent::sub_components::ty
195        sub_component_maps: TiVec<SubComponentIdx, TiVec<SubComponentInstanceIdx, SubComponentIdx>>,
196        /// parent mapping
197        parent_mapping: TiVec<SubComponentIdx, Option<SubComponentIdx>>,
198    }
199
200    impl VisitorState {
201        pub fn new(cu: &CompilationUnit) -> Self {
202            let mut parent_mapping = TiVec::new();
203            parent_mapping.resize(cu.sub_components.len(), None);
204            for (idx, sc) in cu.sub_components.iter_enumerated() {
205                for r in &sc.repeated {
206                    parent_mapping[r.sub_tree.root] = Some(idx);
207                }
208                for p in &sc.popup_windows {
209                    parent_mapping[p.item_tree.root] = Some(idx);
210                }
211                for m in &sc.menu_item_trees {
212                    parent_mapping[m.root] = Some(idx);
213                }
214            }
215            Self {
216                sub_component_maps: cu
217                    .sub_components
218                    .iter()
219                    .map(|sc| sc.sub_components.iter().map(|x| x.ty).collect())
220                    .collect(),
221                parent_mapping,
222            }
223        }
224
225        pub fn follow_sub_components(
226            &self,
227            mut sc: SubComponentIdx,
228            sub_component_path: &[SubComponentInstanceIdx],
229        ) -> SubComponentIdx {
230            for i in sub_component_path {
231                sc = self.sub_component_maps[sc][*i];
232            }
233            sc
234        }
235    }
236
237    pub fn visit_compilation_unit(
238        CompilationUnit {
239            public_components,
240            sub_components,
241            used_sub_components: _,
242            globals,
243            popup_menu,
244            has_debug_info: _,
245            type_exports: _,
246            #[cfg(feature = "bundle-translations")]
247                translations: _,
248        }: &mut crate::llr::CompilationUnit,
249        state: &VisitorState,
250        visitor: &mut (impl Visitor + ?Sized),
251    ) {
252        for c in public_components {
253            visit_public_component(c, state, visitor);
254        }
255        for (idx, sc) in sub_components.iter_mut_enumerated() {
256            visit_sub_component(idx, sc, state, visitor);
257        }
258        for (idx, g) in globals.iter_mut_enumerated() {
259            visit_global(idx, g, state, visitor);
260        }
261        if let Some(p) = popup_menu {
262            visit_popup_menu(p, state, visitor);
263        }
264    }
265
266    pub fn visit_public_component(
267        PublicComponent {
268            public_properties,
269            private_properties: _,
270            item_tree,
271            name: _,
272            top_level_type: _,
273        }: &mut PublicComponent,
274        state: &VisitorState,
275        visitor: &mut (impl Visitor + ?Sized),
276    ) {
277        let scope = EvaluationScope::SubComponent(item_tree.root, None);
278        for p in public_properties.values_mut() {
279            visit_public_property(p, &scope, state, visitor);
280        }
281        visit_tree_node_z_properties(&mut item_tree.tree, &scope, state, visitor);
282    }
283
284    /// The z property paths are relative to the tree root, so `scope` must be the
285    /// scope of the tree root sub-component.
286    fn visit_tree_node_z_properties(
287        node: &mut crate::llr::TreeNode,
288        scope: &EvaluationScope,
289        state: &VisitorState,
290        visitor: &mut (impl Visitor + ?Sized),
291    ) {
292        if let Some(z_props) = &mut node.z_sort_order_property {
293            for z_source in z_props {
294                if let crate::llr::ZSource::Expression(e) = z_source {
295                    visit_expression(e.get_mut(), scope, state, visitor);
296                }
297            }
298        }
299        for child in &mut node.children {
300            visit_tree_node_z_properties(child, scope, state, visitor);
301        }
302    }
303
304    pub fn visit_sub_component(
305        idx: SubComponentIdx,
306        SubComponent {
307            name: _,
308            properties: _,
309            callbacks: _,
310            functions,
311            items: _,
312            repeated,
313            component_containers: _,
314            popup_windows,
315            menu_item_trees: _,
316            timers,
317            sub_components: _,
318            property_init,
319            change_callbacks,
320            animations,
321            two_way_bindings,
322            const_properties,
323            pre_init_code,
324            init_code,
325            geometries,
326            layout_info_h,
327            layout_info_v,
328            child_of_layout: _,
329            grid_layout_input_for_repeated,
330            flexbox_layout_item_info_for_repeated,
331            cross_axis_self_alignment_for_repeated,
332            layout_order_for_repeated,
333            layout_info_v_constrained_for_repeated,
334            layout_info_v_at_cross_width_for_repeated,
335            grid_row_child_cross_width,
336            is_repeated_row: _,
337            grid_layout_children,
338            accessible_prop,
339            element_infos: _,
340            row_child_templates: _,
341            prop_analysis,
342            debug_info: _,
343        }: &mut SubComponent,
344        state: &VisitorState,
345        visitor: &mut (impl Visitor + ?Sized),
346    ) {
347        let scope = EvaluationScope::SubComponent(idx, None);
348        for f in functions {
349            visit_function(f, &scope, state, visitor);
350        }
351        for RepeatedElement {
352            model,
353            index_prop,
354            data_prop,
355            dynamic_z,
356            sub_tree,
357            index_in_tree: _,
358            listview,
359            container_item_index: _,
360        } in repeated
361        {
362            visit_expression(model.get_mut(), &scope, state, visitor);
363            let inner_scope = EvaluationScope::SubComponent(sub_tree.root, None);
364            if let Some(index_prop) = index_prop {
365                visitor.visit_property_idx(index_prop, &inner_scope, state);
366            }
367            if let Some(data_prop) = data_prop {
368                visitor.visit_property_idx(data_prop, &inner_scope, state);
369            }
370            if let Some(dynamic_z) = dynamic_z {
371                visit_member_reference(dynamic_z, &inner_scope, state, visitor);
372            }
373
374            visit_tree_node_z_properties(&mut sub_tree.tree, &inner_scope, state, visitor);
375
376            if let Some(listview) = listview {
377                visit_member_reference(&mut listview.content_y, &scope, state, visitor);
378                if let Some(content_height) = &mut listview.content_height {
379                    visit_member_reference(content_height, &scope, state, visitor);
380                }
381                if let Some(content_width) = &mut listview.content_width {
382                    visit_member_reference(content_width, &scope, state, visitor);
383                }
384                visit_member_reference(&mut listview.listview_width, &scope, state, visitor);
385                visit_member_reference(&mut listview.listview_height, &scope, state, visitor);
386
387                visit_member_reference(&mut listview.prop_y, &inner_scope, state, visitor);
388                visit_member_reference(&mut listview.prop_height, &inner_scope, state, visitor);
389            }
390        }
391
392        for p in popup_windows {
393            let popup_scope = EvaluationScope::SubComponent(p.item_tree.root, None);
394            visit_expression(p.position.get_mut(), &popup_scope, state, visitor);
395            visit_tree_node_z_properties(&mut p.item_tree.tree, &popup_scope, state, visitor);
396        }
397        for t in timers {
398            visit_expression(t.interval.get_mut(), &scope, state, visitor);
399            visit_expression(t.triggered.get_mut(), &scope, state, visitor);
400            visit_expression(t.running.get_mut(), &scope, state, visitor);
401        }
402        for (idx, init) in property_init {
403            visit_member_reference(idx, &scope, state, visitor);
404            visit_binding_expression(init, &scope, state, visitor);
405        }
406        for (idx, e) in change_callbacks {
407            visit_member_reference(idx, &scope, state, visitor);
408            visit_expression(e.get_mut(), &scope, state, visitor);
409        }
410        *animations = std::mem::take(animations)
411            .into_iter()
412            .map(|(mut k, mut v)| {
413                visit_local_member_reference(&mut k, &scope, state, visitor);
414                visit_expression(&mut v, &scope, state, visitor);
415                (k, v)
416            })
417            .collect();
418
419        for twb in two_way_bindings {
420            visit_local_member_reference(&mut twb.prop1, &scope, state, visitor);
421            visit_member_reference(&mut twb.prop2, &scope, state, visitor);
422        }
423        for c in const_properties {
424            visit_local_member_reference(c, &scope, state, visitor);
425        }
426        for i in pre_init_code.iter_mut().chain(init_code) {
427            visit_expression(i.get_mut(), &scope, state, visitor);
428        }
429        for g in geometries.iter_mut().flatten() {
430            visit_expression(g.get_mut(), &scope, state, visitor);
431        }
432        visit_expression(layout_info_h.get_mut(), &scope, state, visitor);
433        visit_expression(layout_info_v.get_mut(), &scope, state, visitor);
434        if let Some(e) = grid_layout_input_for_repeated {
435            visit_expression(e.get_mut(), &scope, state, visitor);
436        }
437        if let Some(e) = flexbox_layout_item_info_for_repeated {
438            visit_expression(e.get_mut(), &scope, state, visitor);
439        }
440        if let Some((_, e)) = cross_axis_self_alignment_for_repeated {
441            visit_expression(e.get_mut(), &scope, state, visitor);
442        }
443        if let Some((_, e)) = layout_order_for_repeated {
444            visit_expression(e.get_mut(), &scope, state, visitor);
445        }
446        if let Some(e) = layout_info_v_constrained_for_repeated {
447            visit_expression(e.get_mut(), &scope, state, visitor);
448        }
449        if let Some(e) = layout_info_v_at_cross_width_for_repeated {
450            visit_expression(e.get_mut(), &scope, state, visitor);
451        }
452        if let Some(e) = grid_row_child_cross_width {
453            visit_expression(e.get_mut(), &scope, state, visitor);
454        }
455        for child in grid_layout_children {
456            visit_expression(child.layout_info_h.get_mut(), &scope, state, visitor);
457            visit_expression(child.layout_info_v.get_mut(), &scope, state, visitor);
458        }
459
460        for a in accessible_prop.values_mut() {
461            visit_expression(a.get_mut(), &scope, state, visitor);
462        }
463
464        *prop_analysis = std::mem::take(prop_analysis)
465            .into_iter()
466            .map(|(mut k, v)| {
467                visit_member_reference(&mut k, &scope, state, visitor);
468                (k, v)
469            })
470            .collect();
471    }
472
473    fn visit_global(
474        global_idx: GlobalIdx,
475        GlobalComponent {
476            name: _,
477            properties: _,
478            callbacks: _,
479            functions,
480            init_values,
481            change_callbacks,
482            const_properties: _,
483            public_properties,
484            private_properties: _,
485            exported: _,
486            aliases: _,
487            is_builtin: _,
488            from_library: _,
489            prop_analysis: _,
490        }: &mut GlobalComponent,
491        state: &VisitorState,
492        visitor: &mut (impl Visitor + ?Sized),
493    ) {
494        let scope = EvaluationScope::Global(global_idx);
495        for f in functions {
496            visit_function(f, &scope, state, visitor);
497        }
498
499        *init_values = std::mem::take(init_values)
500            .into_iter()
501            .map(|(mut k, mut v)| {
502                visit_member_index(&mut k, &scope, state, visitor);
503                visit_binding_expression(&mut v, &scope, state, visitor);
504                (k, v)
505            })
506            .collect();
507
508        *change_callbacks = std::mem::take(change_callbacks)
509            .into_iter()
510            .map(|(mut k, mut v)| {
511                visitor.visit_property_idx(&mut k, &scope, state);
512                visit_expression(v.get_mut(), &scope, state, visitor);
513                (k, v)
514            })
515            .collect();
516
517        for p in public_properties.values_mut() {
518            visit_public_property(p, &scope, state, visitor);
519        }
520    }
521
522    pub fn visit_popup_menu(
523        PopupMenu { item_tree, sub_menu, activated, close, entries }: &mut PopupMenu,
524        state: &VisitorState,
525        visitor: &mut (impl Visitor + ?Sized),
526    ) {
527        let scope = EvaluationScope::SubComponent(item_tree.root, None);
528        visit_member_reference(sub_menu, &scope, state, visitor);
529        visit_member_reference(activated, &scope, state, visitor);
530        visit_member_reference(close, &scope, state, visitor);
531        visit_member_reference(entries, &scope, state, visitor);
532        visit_tree_node_z_properties(&mut item_tree.tree, &scope, state, visitor);
533    }
534
535    pub fn visit_public_property(
536        PublicProperty { prop, .. }: &mut PublicProperty,
537        scope: &EvaluationScope,
538        state: &VisitorState,
539        visitor: &mut (impl Visitor + ?Sized),
540    ) {
541        visit_member_reference(prop, scope, state, visitor);
542    }
543
544    pub fn visit_function(
545        Function { name: _, ret_ty: _, args: _, code, use_count: _ }: &mut Function,
546        scope: &EvaluationScope,
547        state: &VisitorState,
548        visitor: &mut (impl Visitor + ?Sized),
549    ) {
550        visit_expression(code.get_mut(), scope, state, visitor);
551    }
552
553    pub fn visit_expression(
554        expr: &mut Expression,
555        scope: &EvaluationScope,
556        state: &VisitorState,
557        visitor: &mut (impl Visitor + ?Sized),
558    ) {
559        expr.visit_recursive_mut(&mut |expr| {
560            let p = match expr {
561                Expression::PropertyReference(p) => p,
562                Expression::CallBackCall { callback, .. } => callback,
563                Expression::FunctionCall { function, .. } => function,
564                Expression::PropertyAssignment { property, .. } => property,
565                Expression::LayoutCacheAccess { layout_cache_prop, .. } => layout_cache_prop,
566                Expression::GridRepeaterCacheAccess { layout_cache_prop, .. } => layout_cache_prop,
567                _ => return,
568            };
569            visit_member_reference(p, scope, state, visitor);
570        });
571    }
572
573    pub fn visit_binding_expression(
574        BindingExpression { expression, animation, kind: _, use_count: _ }: &mut BindingExpression,
575        scope: &EvaluationScope,
576        state: &VisitorState,
577        visitor: &mut (impl Visitor + ?Sized),
578    ) {
579        visit_expression(expression.get_mut(), scope, state, visitor);
580        match animation {
581            Some(Animation::Static(anim) | Animation::Transition(anim)) => {
582                visit_expression(anim, scope, state, visitor)
583            }
584            None => (),
585        }
586    }
587
588    pub fn visit_member_reference(
589        member: &mut MemberReference,
590        scope: &EvaluationScope,
591        state: &VisitorState,
592        visitor: &mut (impl Visitor + ?Sized),
593    ) {
594        match member {
595            MemberReference::Relative { parent_level, local_reference } => {
596                let &EvaluationScope::SubComponent(mut sc, _) = scope else { unreachable!() };
597                for _ in 0..*parent_level {
598                    sc = state.parent_mapping[sc].unwrap();
599                }
600                let scope = EvaluationScope::SubComponent(sc, None);
601                visit_local_member_reference(local_reference, &scope, state, visitor);
602            }
603            MemberReference::Global { global_index, member } => {
604                let scope = EvaluationScope::Global(*global_index);
605                visit_member_index(member, &scope, state, visitor);
606            }
607        }
608    }
609
610    pub fn visit_local_member_reference(
611        local_reference: &mut LocalMemberReference,
612        scope: &EvaluationScope,
613        state: &VisitorState,
614        visitor: &mut (impl Visitor + ?Sized),
615    ) {
616        let scope = match scope {
617            EvaluationScope::SubComponent(sub_component_idx, _) => EvaluationScope::SubComponent(
618                state
619                    .follow_sub_components(*sub_component_idx, &local_reference.sub_component_path),
620                None,
621            ),
622            scope => *scope,
623        };
624        visit_member_index(&mut local_reference.reference, &scope, state, visitor);
625    }
626
627    pub fn visit_member_index(
628        member: &mut LocalMemberIndex,
629        scope: &EvaluationScope,
630        state: &VisitorState,
631        visitor: &mut (impl Visitor + ?Sized),
632    ) {
633        match member {
634            LocalMemberIndex::Property(p) => {
635                visitor.visit_property_idx(p, scope, state);
636            }
637            LocalMemberIndex::Function(f) => {
638                visitor.visit_function_idx(f, scope, state);
639            }
640            LocalMemberIndex::Callback(c) => {
641                visitor.visit_callback_idx(c, scope, state);
642            }
643            LocalMemberIndex::Native { .. } | LocalMemberIndex::Timer(_) => {}
644        }
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    /// Compile `source`, lower it to the LLR (which runs the optimization passes
651    /// including [`remove_unused`]), and return every declared property name
652    /// across all sub-components. The names are prefixed by the element path, so
653    /// callers match with `contains`.
654    fn lowered_property_names(source: &str) -> std::collections::HashSet<String> {
655        let mut config =
656            crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
657        config.style = Some("fluent".into());
658        let mut diags = crate::diagnostics::BuildDiagnostics::default();
659        let doc_node =
660            crate::parser::parse(source.into(), Some(std::path::Path::new("t.slint")), &mut diags);
661        let (doc, diag, _) =
662            spin_on::spin_on(crate::compile_syntax_node(doc_node, diags, config.clone()));
663        assert!(!diag.has_errors(), "compile error: {:#?}", diag.to_string_vec());
664        let unit = crate::llr::lower_to_item_tree::lower_to_item_tree(&doc, &config);
665        unit.sub_components
666            .iter()
667            .flat_map(|sc| sc.properties.iter().map(|p| p.name.to_string()))
668            .collect()
669    }
670
671    /// The property values below all depend on the `ext` input so they are not
672    /// constant-folded away by the object-tree passes, and therefore actually
673    /// exercise the LLR optimization passes.
674    const SOURCE: &str = r#"
675export component Foo inherits Window {
676    in property <int> ext: 1;
677    in property <int> ext2: 2;
678
679    // KEPT: exposed in the public API.
680    out property <int> kept_public: ext + 1;
681
682    // REMOVED: read only from one other binding, so it is inlined into it and
683    // the reader is itself unused.
684    property <int> gone_inlined: ext * 2;
685    property <int> gone_reader: gone_inlined + 3;
686
687    // REMOVED: cheap enough to inline into each of its two (unused) readers.
688    property <int> gone_shared: ext * 3;
689    property <int> gone_shared_a: gone_shared + 1;
690    property <int> gone_shared_b: gone_shared + 2;
691
692    // KEPT: binding too expensive to inline, and read from two places.
693    property <int> kept_expensive: ext * ext2 + ext2 * ext + ext * ext2;
694    out property <int> exp_a: kept_expensive + 1;
695    out property <int> exp_b: kept_expensive + 2;
696
697    // REMOVED: only read from a timer's interval, which is inlined.
698    property <duration> gone_timer: ext * 1ms;
699
700    // REMOVED: read from a function that is called exactly once. The function is
701    // inlined into its caller, and the property is then inlined and removed.
702    property <int> gone_single_call: ext * 5;
703    pure function called_once() -> int { gone_single_call }
704    out property <int> single_reader: called_once();
705
706    // REMOVED: read only from a function's body. The function itself is not
707    // inlined (it is called from two places), but the property is inlined into
708    // the body, so it becomes unused.
709    property <int> gone_multi_call: ext * 6;
710    pure function called_twice() -> int { gone_multi_call }
711    out property <int> reader_a: called_twice();
712    out property <int> reader_b: called_twice();
713
714    // KEPT: has a change callback.
715    property <int> kept_with_change_callback: ext * 7;
716    changed kept_with_change_callback => {}
717
718    // REMOVED: read only from a callback handler, which is inlined into like a
719    // binding.
720    property <int> gone_callback: ext * 8;
721    callback do_it(int);
722    do_it(v) => { debug(gone_callback + v); }
723
724    Timer { interval: gone_timer; running: true; triggered => { do_it(1); } }
725}
726"#;
727
728    #[test]
729    fn unused_properties_are_removed_and_used_ones_kept() {
730        let names = lowered_property_names(SOURCE);
731        for gone in [
732            "gone-inlined",
733            "gone-reader",
734            "gone-shared",
735            "gone-timer",
736            "gone-single-call",
737            "gone-multi-call",
738            "gone-callback",
739        ] {
740            assert!(
741                !names.iter().any(|n| n.contains(gone)),
742                "property {gone} should have been removed, got {names:?}"
743            );
744        }
745        for kept in ["kept-public", "kept-expensive", "kept-with-change-callback"] {
746            assert!(
747                names.iter().any(|n| n.contains(kept)),
748                "property {kept} should have been kept, got {names:?}"
749            );
750        }
751    }
752}