Skip to main content

i_slint_compiler/passes/
remove_aliases.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//! This pass removes the property used in a two ways bindings
5
6use crate::diagnostics::BuildDiagnostics;
7use crate::expression_tree::{BindingExpression, Expression, NamedReference, TwoWayBinding};
8use crate::langtype::Type;
9use crate::object_tree::*;
10use std::cell::RefCell;
11use std::collections::{HashMap, HashSet};
12use std::rc::Rc;
13
14// The property in the key is to be removed, and replaced by the property in the value
15type Mapping = HashMap<NamedReference, (NamedReference, PropertySet)>;
16type PropertySet = Rc<RefCell<HashSet<NamedReference>>>;
17
18#[derive(Default, Debug)]
19struct PropertySets {
20    map: HashMap<NamedReference, Rc<RefCell<HashSet<NamedReference>>>>,
21    all_sets: Vec<PropertySet>,
22}
23
24impl PropertySets {
25    fn add_link(&mut self, p1: NamedReference, p2: NamedReference) {
26        let mut members = vec![p1.clone(), p2.clone()];
27        for p in [&p1, &p2] {
28            if let Some(s) = self.map.get(p) {
29                members.extend(s.borrow().iter().cloned());
30            }
31        }
32        let initial_component = members[0].element().borrow().enclosing_component.clone();
33        let link_is_same_component = std::rc::Weak::ptr_eq(
34            &p1.element().borrow().enclosing_component,
35            &p2.element().borrow().enclosing_component,
36        );
37        let contains_changed_handlers = members.iter().any(|property| {
38            property.element().borrow().change_callbacks.contains_key(property.name())
39        });
40        let set_is_same_component = members.iter().all(|property| {
41            std::rc::Weak::ptr_eq(
42                &initial_component,
43                &property.element().borrow().enclosing_component,
44            )
45        });
46        let link_involves_global = [&p1, &p2].iter().any(|property| {
47            property.element().borrow().enclosing_component.upgrade().unwrap().is_global()
48        });
49
50        if !link_is_same_component {
51            // We can only add a new link across components if the link involves a global and none
52            // of the involved bindings (including earlier aliases) have a changed handler.
53            let can_merge_across_components = !contains_changed_handlers && link_involves_global;
54            if !can_merge_across_components {
55                return;
56            }
57        } else {
58            // the new link is within the same component,
59            // but the previously processed aliases may already contain another component (which must be a
60            // global due to the `if` path.
61            // If that is the case, only alias if no changed handlers are involved.
62            let can_merge_within_component = !contains_changed_handlers || set_is_same_component;
63            if !can_merge_within_component {
64                return;
65            }
66        }
67
68        if let Some(s1) = self.map.get(&p1).cloned() {
69            if let Some(s2) = self.map.get(&p2).cloned() {
70                if Rc::ptr_eq(&s1, &s2) {
71                    return;
72                }
73                for x in s1.borrow().iter() {
74                    self.map.insert(x.clone(), s2.clone());
75                    s2.borrow_mut().insert(x.clone());
76                }
77                *s1.borrow_mut() = HashSet::new();
78            } else {
79                s1.borrow_mut().insert(p2.clone());
80                self.map.insert(p2, s1);
81            }
82        } else if let Some(s2) = self.map.get(&p2).cloned() {
83            s2.borrow_mut().insert(p1.clone());
84            self.map.insert(p1, s2);
85        } else {
86            let mut set = HashSet::new();
87            set.insert(p1.clone());
88            set.insert(p2.clone());
89            let set = Rc::new(RefCell::new(set));
90            self.map.insert(p1, set.clone());
91            self.map.insert(p2, set.clone());
92            self.all_sets.push(set)
93        }
94    }
95}
96
97pub fn remove_aliases(doc: &Document, diag: &mut BuildDiagnostics) {
98    // collect all sets that are linked together
99    let mut property_sets = PropertySets::default();
100
101    // Track how many distinct declarations alias into the same target
102    // target -> [aliases]
103    let mut callback_aliases: HashMap<NamedReference, Vec<NamedReference>> = HashMap::new();
104
105    let mut process_element = |e: &ElementRc| {
106        'bindings: for (name, binding) in e.borrow().real_bindings() {
107            for twb in &binding.borrow().two_way_bindings {
108                if let TwoWayBinding::Property { property, field_access } = twb {
109                    if !field_access.is_empty() {
110                        // Don't optimize two way bindings to fields for now
111                        continue;
112                    }
113                    let other_e = property.element();
114                    if name == property.name() && Rc::ptr_eq(e, &other_e) {
115                        diag.push_error(
116                            "Property cannot alias to itself".into(),
117                            &*binding.borrow(),
118                        );
119                        continue 'bindings;
120                    }
121                    let source = NamedReference::new(e, name.clone());
122                    if matches!(source.ty(), Type::Callback(..)) {
123                        let bucket = callback_aliases.entry(property.clone()).or_default();
124                        if !bucket.contains(&source) {
125                            bucket.push(source.clone());
126                        }
127                    }
128                    property_sets.add_link(source, property.clone());
129                }
130            }
131        }
132    };
133
134    doc.visit_all_used_components(|component| {
135        recurse_elem_including_sub_components(component, &(), &mut |e, &()| process_element(e))
136    });
137
138    for (target, sources) in &callback_aliases {
139        if sources.len() < 2 {
140            continue;
141        }
142        // if a callback has an implementation, the rest are alternate names for invoking it
143        let has_implementation = property_sets.map.get(target).is_some_and(|set_rc| {
144            set_rc.borrow().iter().any(|nr| {
145                let elem = nr.element();
146                let elem = elem.borrow();
147                elem.binding(nr.name())
148                    .is_some_and(|b| !matches!(b.value_expression(), Expression::Invalid))
149            })
150        });
151        if has_implementation {
152            continue;
153        }
154        for nr in sources {
155            let other_names = sources
156                .iter()
157                .filter(|other| *other != nr)
158                .map(|other| format!("'{}'", other.name()))
159                .collect::<Vec<_>>()
160                .join(", ");
161            let elem = nr.element();
162            let elem = elem.borrow();
163            if let Some(b) = elem.binding(nr.name()) {
164                // Only a warning: this compiled fine up to 1.17 and code relies on it.
165                diag.push_warning(
166                    format!(
167                        "Callback '{}' shares a handler slot with {other_names}, so only one of them can have an implementation",
168                        nr.name()
169                    ),
170                    &*b,
171                );
172            }
173        }
174    }
175
176    // The key will be removed and replaced by the named reference
177    let mut aliases_to_remove = Mapping::new();
178
179    // For each set, find a "master" property. Only reference to this master property will be kept,
180    // and only the master property will keep its binding
181    for set_rc in property_sets.all_sets {
182        let set = set_rc.borrow();
183
184        // Globals are singletons, so a callback aliased across globals must have at most
185        // one implementation. More than one handler in the set is an ambiguous conflict.
186        // (For non-global elements multiple handlers are fine: instances legitimately
187        // override a base's handler, resolved below by binding priority.)
188        let implementers: Vec<NamedReference> = set
189            .iter()
190            .filter(|nr| {
191                if !matches!(nr.ty(), Type::Callback(..)) {
192                    return false;
193                }
194                let elem = nr.element();
195                let elem = elem.borrow();
196                elem.enclosing_component.upgrade().is_some_and(|c| c.is_global())
197                    && elem
198                        .binding(nr.name())
199                        .is_some_and(|b| !matches!(b.value_expression(), Expression::Invalid))
200            })
201            .cloned()
202            .collect();
203        if implementers.len() > 1 {
204            for nr in &implementers {
205                let elem = nr.element();
206                let elem = elem.borrow();
207                if let Some(b) = elem.binding(nr.name()) {
208                    diag.push_error(
209                        format!("Callback '{}' is implemented in more than one global", nr.name()),
210                        &*b,
211                    );
212                }
213            }
214        }
215
216        let mut set_iter = set.iter();
217        if let Some(mut best) = set_iter.next().cloned() {
218            for candidate in set_iter {
219                best = best_property(best.clone(), candidate.clone());
220            }
221            let best_is_global =
222                best.element().borrow().enclosing_component.upgrade().unwrap().is_global();
223            for x in set.iter() {
224                if *x != best {
225                    // Keep a property with a `changed` handler or an animation in its own
226                    // component rather than aliasing it onto a global (a singleton it can't
227                    // reach); the replacement below leaves it two-way bound to `best`.
228                    let elem = x.element();
229                    let elem = elem.borrow();
230                    let carries_local_state = elem.change_callbacks.contains_key(x.name())
231                        || elem
232                            .binding(x.name())
233                            .is_some_and(|binding| binding.animation.is_some());
234                    if best_is_global
235                        && !elem.enclosing_component.upgrade().unwrap().is_global()
236                        && carries_local_state
237                    {
238                        continue;
239                    }
240                    aliases_to_remove.insert(x.clone(), (best.clone(), Rc::clone(&set_rc)));
241                }
242            }
243        }
244    }
245
246    doc.visit_all_used_components(|component| {
247        // Do the replacements
248        visit_all_named_references(component, &mut |nr: &mut NamedReference| {
249            if let Some((new, _set)) = aliases_to_remove.get(nr) {
250                *nr = new.clone();
251            }
252        })
253    });
254
255    // Process the removal in a deterministic order to ensure that changed callbacks
256    // are always merged in the same order.
257    let mut aliases_to_remove = aliases_to_remove.into_iter().collect::<Vec<_>>();
258    aliases_to_remove.sort_by_cached_key(|(remove, _)| {
259        (remove.element().borrow().id.clone(), remove.name().clone())
260    });
261    for (remove, (to, set)) in aliases_to_remove {
262        let elem = remove.element();
263        let to_elem = to.element();
264
265        // adjust the bindings
266        let old_binding = elem.borrow_mut().take_binding(remove.name());
267        let mut old_binding = old_binding.unwrap_or_else(|| {
268            // ensure that we set an expression, because the right hand side of a binding always wins,
269            // and if that was not set, we must still keep the default then
270            let mut b = BindingExpression::from(Expression::default_value_for_type(&to.ty()));
271            b.priority = to_elem
272                .borrow()
273                .binding(to.name())
274                .map_or(i32::MAX, |to_binding| to_binding.priority.saturating_add(1));
275            b
276        });
277
278        remove_from_binding_expression(&mut old_binding, &to);
279
280        // When the master `to` is a global, re-home two-way bindings whose target is *not* in this
281        // set onto the surviving target instead of merging them onto `to`.
282        //
283        // A two-way binding to a property outside this set only survives here because `add_link`
284        // *rejected* merging it into the set (e.g. it would have pulled a `changed` handler across
285        // into a global). The two endpoints are still meant to be linked at runtime, but the rejected
286        // target was never folded away, so we must not let `old_binding` carry that reference onto `to`
287        // via the `merge_with` below: that would leave the global singleton holding a reference to an
288        // instance element it cannot resolve ("accessing deleted parent" at runtime).
289        //
290        // Instead, re-express the link from the surviving target's side as `target <=> to`. This is
291        // only sound because `to` is a global: at runtime an element-to-global reference is resolved by
292        // a direct global lookup (`enclosing_component_instance_for_element`), so `target` can always
293        // resolve `to` regardless of where `target` lives in the tree.
294        //
295        // The reverse is *not* true, which is why this is gated on `to` being a global: when `to` is an
296        // instance, references to it are resolved by walking *up* the parent chain from the hosting
297        // element (`enclosing_component_for_element`). The normal `merge_with` keeps the binding on `to`
298        // and references `target` -- the same direction the original `remove <=> target` binding used,
299        // so it is known to resolve. Flipping it to host on `target` and reference the instance `to`
300        // would require `target`'s context to reach `to`, which is not guaranteed (e.g. `to` lives in a
301        // nested sub-component) and panics with the same "accessing deleted parent" -- as observed when
302        // this guard is dropped (e.g. the `todo` demo).
303        if to_elem.borrow().enclosing_component.upgrade().unwrap().is_global() {
304            old_binding.two_way_bindings.retain(|twb| {
305                let TwoWayBinding::Property { property, field_access } = twb else { return true };
306                if !field_access.is_empty() || set.borrow().contains(property) {
307                    // Field-access bindings aren't aliased (see above), and a target that *is* in
308                    // the set was folded into `to` already, so the normal merge below handles it.
309                    return true;
310                }
311                let target_elem = property.element();
312                let mut target_elem = target_elem.borrow_mut();
313                let mut target_binding = if let Some(b) = target_elem.binding_mut(property.name()) {
314                    // need to unwrap manually here so the borrow checker understands that
315                    // target_elem is no longer borrowed in the `else` path.
316                    b
317                } else {
318                    target_elem.set_binding(
319                        property.name().clone(),
320                        BindingExpression::new_two_way(to.clone().into()),
321                    );
322                    target_elem.binding_mut(property.name()).unwrap()
323                };
324                // let b = target_elem.binding_mut(property.name()).expect("Binding was just set");
325                if !target_binding.two_way_bindings.iter().any(|x| x.property() == Some(&to)) {
326                    target_binding.two_way_bindings.push(to.clone().into());
327                }
328                // drop from old_binding so the merge below won't carry it onto the global
329                false
330            });
331        }
332
333        let same_component = std::rc::Weak::ptr_eq(
334            &elem.borrow().enclosing_component,
335            &to_elem.borrow().enclosing_component,
336        );
337        // Globals are singletons, so a handler an aliasing global provides for another
338        // global's callback must be carried over to the master, just like within a component.
339        let both_global =
340            elem.borrow().enclosing_component.upgrade().is_some_and(|c| c.is_global())
341                && to_elem.borrow().enclosing_component.upgrade().is_some_and(|c| c.is_global());
342        {
343            let mut to_elem = to_elem.borrow_mut();
344            // use if let else here instead of match so that to_elem is no longer borrowed
345            // in the else path.
346            if let Some(mut b) = to_elem.binding_mut(to.name()) {
347                let b = &mut *b;
348                remove_from_binding_expression(b, &to);
349                if !same_component || b.priority < old_binding.priority || !b.has_binding() {
350                    b.merge_with(&old_binding);
351                } else {
352                    old_binding.merge_with(b);
353                    *b = old_binding;
354                }
355            } else {
356                if (same_component || both_global)
357                    && (old_binding.has_binding() || old_binding.animation.is_some())
358                {
359                    to_elem.set_binding(to.name().clone(), old_binding);
360                }
361            }
362        }
363
364        // Adjust the change callbacks
365        {
366            let mut elem = elem.borrow_mut();
367            if let Some(old_change_callback) = elem.change_callbacks.remove(remove.name()) {
368                drop(elem);
369                let mut old_change_callback = old_change_callback.into_inner();
370                to_elem
371                    .borrow_mut()
372                    .change_callbacks
373                    .entry(to.name().clone())
374                    .or_default()
375                    .borrow_mut()
376                    .append(&mut old_change_callback);
377            }
378        }
379
380        // Remove the declaration
381        {
382            let mut elem = elem.borrow_mut();
383            let used_externally = elem
384                .property_analysis
385                .borrow()
386                .get(remove.name())
387                .is_some_and(|v| v.is_read_externally || v.is_set_externally);
388            if let Some(d) = elem.property_declarations.get_mut(remove.name()) {
389                if d.expose_in_public_api || used_externally {
390                    d.is_alias = Some(to.clone());
391                    drop(elem);
392                    // one must mark the aliased property as settable from outside
393                    to.mark_as_set();
394                } else {
395                    elem.property_declarations.remove(remove.name());
396                    let analysis = elem.property_analysis.borrow().get(remove.name()).cloned();
397                    if let Some(analysis) = analysis {
398                        drop(elem);
399                        to.element()
400                            .borrow()
401                            .property_analysis
402                            .borrow_mut()
403                            .entry(to.name().clone())
404                            .or_default()
405                            .merge(&analysis);
406                    };
407                }
408            } else {
409                // This is not a declaration, we must re-create the binding
410                elem.set_binding(
411                    remove.name().clone(),
412                    BindingExpression::new_two_way(to.clone().into()),
413                );
414                drop(elem);
415                if remove.is_externally_modified() {
416                    to.mark_as_set();
417                }
418            }
419        }
420    }
421}
422
423fn is_declaration(x: &NamedReference) -> bool {
424    x.element().borrow().property_declarations.contains_key(x.name())
425}
426
427/// Out of two named reference, return the one which is the best to keep.
428fn best_property(p1: NamedReference, p2: NamedReference) -> NamedReference {
429    // Try to find which is the more canonical property
430    macro_rules! canonical_order {
431        ($x: expr) => {{
432            (
433                !$x.element().borrow().enclosing_component.upgrade().unwrap().is_global(),
434                is_declaration(&$x),
435                $x.element().borrow().id.clone(),
436                $x.name(),
437            )
438        }};
439    }
440
441    if canonical_order!(p1) < canonical_order!(p2) { p1 } else { p2 }
442}
443
444/// Remove the `to` from the two_way_bindings
445fn remove_from_binding_expression(expression: &mut BindingExpression, to: &NamedReference) {
446    expression.two_way_bindings.retain(|x| x.property() != Some(to));
447}