Skip to main content

i_slint_compiler/passes/
binding_analysis.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//! Compute binding analysis and attempt to find binding loops
5
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::rc::Rc;
9
10use by_address::ByAddress;
11
12use crate::diagnostics::{BuildDiagnostics, Spanned};
13use crate::expression_tree::{BindingExpression, BuiltinFunction, Expression};
14use crate::langtype::ElementType;
15use crate::layout::{LayoutItem, Orientation};
16use crate::namedreference::NamedReference;
17use crate::object_tree::{Document, ElementRc, PropertyAnimation, find_parent_element};
18use derive_more as dm;
19
20use crate::CompilerConfiguration;
21use crate::expression_tree::Callable;
22use smol_str::{SmolStr, ToSmolStr};
23
24/// Represent the kind of property for the DefaultFontSize based on the `default-font-size`` property of every `Window``
25#[derive(Debug, Clone, PartialEq, Default)]
26pub enum DefaultFontSize {
27    /// Not yet known/computed
28    #[default]
29    Unknown,
30    /// The default font size is set to a specific constant value in `px` (logical)
31    LogicalValue(f32),
32    /// The default font size is set to different, but always constant values
33    Const,
34    /// All windows are either using const value or unset
35    NotSet,
36    /// At least one `Window` has a non-constant default-font-size
37    Variable,
38}
39impl DefaultFontSize {
40    /// Returns true if the default font size is a constant value
41    pub fn is_const(&self) -> bool {
42        // Note that NotSet is considered const for now as the renderer won't change the default font size at runtime
43        matches!(self, Self::Const | Self::LogicalValue(_))
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Default)]
48pub struct GlobalAnalysis {
49    pub default_font_size: DefaultFontSize,
50    pub const_scale_factor: Option<f32>,
51}
52
53/// Maps the alias in the other direction than what the BindingExpression::two_way_binding does.
54/// So if binding for property A has B in its BindingExpression::two_way_binding, then
55/// ReverseAliases maps B to A.
56type ReverseAliases = HashMap<NamedReference, Vec<NamedReference>>;
57
58pub fn binding_analysis(
59    doc: &Document,
60    compiler_config: &CompilerConfiguration,
61    diag: &mut BuildDiagnostics,
62) -> GlobalAnalysis {
63    let mut global_analysis = GlobalAnalysis {
64        const_scale_factor: compiler_config.const_scale_factor,
65        ..Default::default()
66    };
67    let mut reverse_aliases = Default::default();
68    mark_used_base_properties(doc);
69    propagate_is_set_on_aliases(doc, &mut reverse_aliases);
70    check_window_properties(doc, &mut global_analysis);
71    perform_binding_analysis(
72        doc,
73        &reverse_aliases,
74        &mut global_analysis,
75        compiler_config.error_on_binding_loop_with_window_layout,
76        diag,
77    );
78    global_analysis
79}
80/// A reference to a property which might be deep in a component path.
81/// eg: `foo.bar.baz.background`: `baz.background` is the `prop` and `foo` and `bar` are in elements
82#[derive(Hash, PartialEq, Eq, Clone)]
83struct PropertyPath {
84    elements: Vec<ByAddress<ElementRc>>,
85    prop: NamedReference,
86}
87
88impl std::fmt::Debug for PropertyPath {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        for e in &self.elements {
91            write!(f, "{}.", e.borrow().id)?;
92        }
93        self.prop.fmt(f)
94    }
95}
96
97impl PropertyPath {
98    /// Given a namedReference accessed by something on the same leaf component
99    /// as self, return a new PropertyPath that represent the property pointer
100    /// to by nr in the higher possible element
101    fn relative(&self, second: &PropertyPath) -> Self {
102        let mut element =
103            second.elements.first().map_or_else(|| second.prop.element(), |f| f.0.clone());
104        if element.borrow().enclosing_component.upgrade().unwrap().is_global() {
105            return second.clone();
106        }
107        let mut elements = self.elements.clone();
108        loop {
109            let enclosing = element.borrow().enclosing_component.upgrade().unwrap();
110            if enclosing.parent_element().is_some()
111                || !Rc::ptr_eq(&element, &enclosing.root_element)
112            {
113                break;
114            }
115
116            if let Some(last) = elements.pop() {
117                #[cfg(debug_assertions)]
118                fn check_that_element_is_in_the_component(
119                    e: &ElementRc,
120                    c: &Rc<crate::object_tree::Component>,
121                ) -> bool {
122                    let enclosing = e.borrow().enclosing_component.upgrade().unwrap();
123                    Rc::ptr_eq(c, &enclosing)
124                        || enclosing
125                            .parent_element
126                            .borrow()
127                            .upgrade()
128                            .is_some_and(|e| check_that_element_is_in_the_component(&e, c))
129                }
130                #[cfg(debug_assertions)]
131                debug_assert!(
132                    check_that_element_is_in_the_component(
133                        &element,
134                        last.borrow().base_type.as_component()
135                    ),
136                    "The element is not in the component pointed at by the path ({self:?} / {second:?})"
137                );
138                element = last.0;
139            } else {
140                break;
141            }
142        }
143        if second.elements.is_empty() {
144            debug_assert!(elements.last().is_none_or(|x| *x != ByAddress(second.prop.element())));
145            Self { elements, prop: NamedReference::new(&element, second.prop.name().clone()) }
146        } else {
147            elements.push(ByAddress(element));
148            elements.extend(second.elements.iter().skip(1).cloned());
149            Self { elements, prop: second.prop.clone() }
150        }
151    }
152}
153
154impl From<NamedReference> for PropertyPath {
155    fn from(prop: NamedReference) -> Self {
156        Self { elements: Vec::new(), prop }
157    }
158}
159
160struct AnalysisContext<'a> {
161    visited: HashSet<PropertyPath>,
162    /// The stack of properties that depends on each other
163    currently_analyzing: linked_hash_set::LinkedHashSet<PropertyPath>,
164    /// When set, one of the property in the `currently_analyzing` stack is the window layout property
165    /// And we should issue a warning if that's part of a loop instead of an error
166    window_layout_property: Option<PropertyPath>,
167    error_on_binding_loop_with_window_layout: bool,
168    global_analysis: &'a mut GlobalAnalysis,
169}
170
171fn perform_binding_analysis(
172    doc: &Document,
173    reverse_aliases: &ReverseAliases,
174    global_analysis: &mut GlobalAnalysis,
175    error_on_binding_loop_with_window_layout: bool,
176    diag: &mut BuildDiagnostics,
177) {
178    let mut context = AnalysisContext {
179        error_on_binding_loop_with_window_layout,
180        visited: HashSet::new(),
181        currently_analyzing: Default::default(),
182        window_layout_property: None,
183        global_analysis,
184    };
185    doc.visit_all_used_components(|component| {
186        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
187            component,
188            &(),
189            &mut |e, _| analyze_element(e, &mut context, reverse_aliases, diag),
190        )
191    });
192}
193
194fn analyze_element(
195    elem: &ElementRc,
196    context: &mut AnalysisContext,
197    reverse_aliases: &ReverseAliases,
198    diag: &mut BuildDiagnostics,
199) {
200    for (name, binding) in &elem.borrow().bindings {
201        if binding.borrow().analysis.is_some() {
202            continue;
203        }
204        analyze_binding(
205            &PropertyPath::from(NamedReference::new(elem, name.clone())),
206            context,
207            reverse_aliases,
208            diag,
209        );
210    }
211    for cb in elem.borrow().change_callbacks.values() {
212        for e in cb.borrow().iter() {
213            recurse_expression(elem, e, &mut |prop, r| {
214                process_property(prop, r, context, reverse_aliases, diag);
215            });
216        }
217    }
218    const P: ReadType = ReadType::PropertyRead;
219    for nr in elem.borrow().accessibility_props.0.values() {
220        process_property(&PropertyPath::from(nr.clone()), P, context, reverse_aliases, diag);
221    }
222    if let Some(g) = elem.borrow().geometry_props.as_ref() {
223        process_property(&g.x.clone().into(), P, context, reverse_aliases, diag);
224        process_property(&g.y.clone().into(), P, context, reverse_aliases, diag);
225        process_property(&g.width.clone().into(), P, context, reverse_aliases, diag);
226        process_property(&g.height.clone().into(), P, context, reverse_aliases, diag);
227    }
228
229    if let Some(component) = elem.borrow().enclosing_component.upgrade()
230        && Rc::ptr_eq(&component.root_element, elem)
231    {
232        for e in component.init_code.borrow().iter() {
233            recurse_expression(elem, e, &mut |prop, r| {
234                process_property(prop, r, context, reverse_aliases, diag);
235            });
236        }
237        component.root_constraints.borrow_mut().visit_named_references(&mut |nr| {
238            process_property(&nr.clone().into(), P, context, reverse_aliases, diag);
239        });
240        component.popup_windows.borrow().iter().for_each(|p| {
241            process_property(&p.x.clone().into(), P, context, reverse_aliases, diag);
242            process_property(&p.y.clone().into(), P, context, reverse_aliases, diag);
243        });
244        component.timers.borrow().iter().for_each(|t| {
245            process_property(&t.interval.clone().into(), P, context, reverse_aliases, diag);
246            process_property(&t.running.clone().into(), P, context, reverse_aliases, diag);
247            process_property(&t.triggered.clone().into(), P, context, reverse_aliases, diag);
248        });
249    }
250
251    if let Some(repeated) = &elem.borrow().repeated {
252        recurse_expression(elem, &repeated.model, &mut |prop, r| {
253            process_property(prop, r, context, reverse_aliases, diag);
254        });
255        if let Some(lv) = &repeated.is_listview {
256            process_property(&lv.viewport_y.clone().into(), P, context, reverse_aliases, diag);
257            process_property(&lv.viewport_height.clone().into(), P, context, reverse_aliases, diag);
258            process_property(&lv.viewport_width.clone().into(), P, context, reverse_aliases, diag);
259            process_property(&lv.listview_height.clone().into(), P, context, reverse_aliases, diag);
260            process_property(&lv.listview_width.clone().into(), P, context, reverse_aliases, diag);
261        }
262    }
263    if let Some((h, v)) = &elem.borrow().layout_info_prop {
264        process_property(&h.clone().into(), P, context, reverse_aliases, diag);
265        process_property(&v.clone().into(), P, context, reverse_aliases, diag);
266    }
267
268    for info in elem.borrow().debug.iter() {
269        if let Some(crate::layout::Layout::GridLayout(grid)) = &info.layout
270            && grid.uses_auto
271        {
272            for rowcol_prop_name in ["row", "col"] {
273                for it in grid.elems.iter() {
274                    let child = &it.item.element;
275                    if child
276                        .borrow()
277                        .property_analysis
278                        .borrow()
279                        .get(rowcol_prop_name)
280                        .is_some_and(|a| a.is_set || a.is_set_externally)
281                    {
282                        diag.push_error(
283                            format!("Cannot set property '{}' on '{}' because parent GridLayout uses auto-numbering",
284                                rowcol_prop_name, child.borrow().id),
285                                &child.borrow().to_source_location(), // not ideal, the location of the property being set would be better
286                        );
287                    }
288                }
289            }
290        }
291    }
292}
293
294#[derive(Copy, Clone, dm::BitAnd, dm::BitOr, dm::BitAndAssign, dm::BitOrAssign)]
295struct DependsOnExternal(bool);
296
297fn analyze_binding(
298    current: &PropertyPath,
299    context: &mut AnalysisContext,
300    reverse_aliases: &ReverseAliases,
301    diag: &mut BuildDiagnostics,
302) -> DependsOnExternal {
303    let mut depends_on_external = DependsOnExternal(false);
304    let element = current.prop.element();
305    let name = current.prop.name();
306    if (context.currently_analyzing.back() == Some(current))
307        && !element.borrow().bindings[name].borrow().two_way_bindings.is_empty()
308    {
309        let span = element.borrow().bindings[name]
310            .borrow()
311            .span
312            .clone()
313            .unwrap_or_else(|| element.borrow().to_source_location());
314        diag.push_error(format!("Property '{name}' cannot refer to itself"), &span);
315        return depends_on_external;
316    }
317
318    if context.currently_analyzing.contains(current) {
319        let mut loop_description = String::new();
320        let mut has_window_layout = false;
321
322        fn push_prop(prop: &PropertyPath, out: &mut String) {
323            if !out.is_empty() {
324                out.push_str(" -> ");
325            }
326            match prop.prop.element().borrow().id.as_str() {
327                "" => out.push_str(prop.prop.name()),
328                id => {
329                    out.push_str(id);
330                    out.push('.');
331                    out.push_str(prop.prop.name());
332                }
333            }
334        }
335
336        // Build description by iterating in reverse (trigger direction: "A triggers B")
337        // and close the loop by prepending `current` at the start.
338        push_prop(current, &mut loop_description);
339        for it in context.currently_analyzing.iter().rev() {
340            if context.window_layout_property.as_ref().is_some_and(|p| p == it) {
341                has_window_layout = true;
342            }
343            push_prop(it, &mut loop_description);
344            if it == current {
345                break;
346            }
347        }
348
349        for it in context.currently_analyzing.iter().rev() {
350            let p = &it.prop;
351            let elem = p.element();
352            let elem = elem.borrow();
353            let binding = elem.bindings[p.name()].borrow();
354            if binding.analysis.as_ref().unwrap().is_in_binding_loop.replace(true) {
355                break;
356            }
357
358            let span = binding.span.clone().unwrap_or_else(|| elem.to_source_location());
359            if !context.error_on_binding_loop_with_window_layout && has_window_layout {
360                diag.push_warning(format!("The binding for the property '{}' is part of a binding loop ({loop_description}).\nThis was allowed in previous version of Slint, but is deprecated and may cause panic at runtime", p.name()), &span);
361            } else {
362                diag.push_error(format!("The binding for the property '{}' is part of a binding loop ({loop_description})", p.name()), &span);
363            }
364            if it == current {
365                break;
366            }
367        }
368        return depends_on_external;
369    }
370
371    let binding = &element.borrow().bindings[name];
372    if binding.borrow().analysis.as_ref().is_some_and(|a| a.no_external_dependencies) {
373        return depends_on_external;
374    } else if !context.visited.insert(current.clone()) {
375        return DependsOnExternal(true);
376    }
377
378    if let Ok(mut b) = binding.try_borrow_mut() {
379        b.analysis = Some(Default::default());
380    };
381    context.currently_analyzing.insert(current.clone());
382
383    let b = binding.borrow();
384    for twb in &b.two_way_bindings {
385        if twb.property != current.prop {
386            depends_on_external |= process_property(
387                &current.relative(&twb.property.clone().into()),
388                ReadType::PropertyRead,
389                context,
390                reverse_aliases,
391                diag,
392            );
393        }
394    }
395
396    let mut process_prop = |prop: &PropertyPath, r, context: &mut AnalysisContext| {
397        depends_on_external |=
398            process_property(&current.relative(prop), r, context, reverse_aliases, diag);
399        for x in reverse_aliases.get(&prop.prop).unwrap_or(&Default::default()) {
400            if x != &current.prop && x != &prop.prop {
401                depends_on_external |= process_property(
402                    &current.relative(&x.clone().into()),
403                    ReadType::PropertyRead,
404                    context,
405                    reverse_aliases,
406                    diag,
407                );
408            }
409        }
410    };
411
412    recurse_expression(&current.prop.element(), &b.expression, &mut |p, r| {
413        process_prop(p, r, context)
414    });
415
416    let mut is_const = b.expression.is_constant(Some(context.global_analysis))
417        && b.two_way_bindings.iter().all(|n| n.property.is_constant());
418
419    if is_const && matches!(b.expression, Expression::Invalid) {
420        // check the base
421        if let Some(base) = element.borrow().sub_component() {
422            is_const = NamedReference::new(&base.root_element, name.clone()).is_constant();
423        }
424    }
425    drop(b);
426
427    if let Ok(mut b) = binding.try_borrow_mut() {
428        // We have a loop (through different component so we're still borrowed)
429        b.analysis.as_mut().unwrap().is_const = is_const;
430    }
431
432    match &binding.borrow().animation {
433        Some(PropertyAnimation::Static(e)) => analyze_element(e, context, reverse_aliases, diag),
434        Some(PropertyAnimation::Transition { animations, state_ref }) => {
435            recurse_expression(&current.prop.element(), state_ref, &mut |p, r| {
436                process_prop(p, r, context)
437            });
438            for a in animations {
439                analyze_element(&a.animation, context, reverse_aliases, diag);
440            }
441        }
442        None => (),
443    }
444
445    let o = context.currently_analyzing.pop_back();
446    assert_eq!(&o.unwrap(), current);
447
448    depends_on_external
449}
450
451#[derive(Copy, Clone, Eq, PartialEq)]
452enum ReadType {
453    // Read from the native code
454    NativeRead,
455    // Read from another property binding in Slint
456    PropertyRead,
457}
458
459/// Process the property `prop`
460///
461/// This will visit all the bindings from that property
462fn process_property(
463    prop: &PropertyPath,
464    read_type: ReadType,
465    context: &mut AnalysisContext,
466    reverse_aliases: &ReverseAliases,
467    diag: &mut BuildDiagnostics,
468) -> DependsOnExternal {
469    #[allow(clippy::match_single_binding)]
470    let depends_on_external = match prop
471        .prop
472        .element()
473        .borrow()
474        .property_analysis
475        .borrow_mut()
476        .entry(prop.prop.name().clone())
477        .or_default()
478    {
479        a => {
480            if read_type == ReadType::PropertyRead {
481                a.is_read = true;
482            }
483            DependsOnExternal(prop.elements.is_empty() && a.is_set_externally)
484        }
485    };
486
487    let mut prop = prop.clone();
488
489    loop {
490        let element = prop.prop.element();
491        if element.borrow().bindings.contains_key(prop.prop.name()) {
492            analyze_binding(&prop, context, reverse_aliases, diag);
493            break;
494        }
495        let next = match &element.borrow().base_type {
496            ElementType::Component(base) => {
497                if element.borrow().property_declarations.contains_key(prop.prop.name()) {
498                    break;
499                }
500                base.root_element.clone()
501            }
502            ElementType::Builtin(builtin) => {
503                if builtin.properties.contains_key(prop.prop.name()) {
504                    visit_builtin_property(builtin, &prop, context, reverse_aliases, diag);
505                }
506                break;
507            }
508            _ => break,
509        };
510        next.borrow()
511            .property_analysis
512            .borrow_mut()
513            .entry(prop.prop.name().clone())
514            .or_default()
515            .is_read_externally = true;
516        prop.elements.push(element.into());
517        prop.prop = NamedReference::new(&next, prop.prop.name().clone());
518    }
519    depends_on_external
520}
521
522// Same as in crate::visit_all_named_references_in_element, but not mut
523fn recurse_expression(
524    elem: &ElementRc,
525    expr: &Expression,
526    vis: &mut impl FnMut(&PropertyPath, ReadType),
527) {
528    const P: ReadType = ReadType::PropertyRead;
529    expr.visit(|sub| recurse_expression(elem, sub, vis));
530    match expr {
531        Expression::PropertyReference(r) => vis(&r.clone().into(), P),
532        Expression::LayoutCacheAccess { layout_cache_prop, .. } => {
533            vis(&layout_cache_prop.clone().into(), P)
534        }
535        Expression::GridRepeaterCacheAccess { layout_cache_prop, .. } => {
536            vis(&layout_cache_prop.clone().into(), P)
537        }
538        Expression::SolveBoxLayout(l, o) | Expression::ComputeBoxLayoutInfo(l, o) => {
539            // we should only visit the layout geometry for the orientation
540            if matches!(expr, Expression::SolveBoxLayout(..))
541                && let Some(nr) = l.geometry.rect.size_reference(*o)
542            {
543                vis(&nr.clone().into(), P);
544            }
545            visit_layout_items_dependencies(l.elems.iter(), *o, vis);
546
547            let mut g = l.geometry.clone();
548            g.rect = Default::default(); // already visited;
549            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
550        }
551        Expression::SolveFlexboxLayout(layout)
552        | Expression::ComputeFlexboxLayoutInfo(layout, _) => {
553            if let Some(nr) = layout.direction.as_ref() {
554                vis(&nr.clone().into(), P);
555            }
556            // Visit layout geometry dependencies
557            if matches!(expr, Expression::SolveFlexboxLayout(..)) {
558                // The solve needs the main-axis dimension (width for row,
559                // height for column). On the cross axis, *builtin* items
560                // receive the perpendicular size through the item VTable's
561                // `cross_axis_constraint` parameter and never read `self.width`
562                // at runtime, so no edge is declared for them here. *Component*
563                // items don't have that shortcut: their compiled
564                // `layoutinfo-<cross>` binding is evaluated as an ordinary
565                // property and really does depend on the cross-axis dimension
566                // (typically via inner height-for-width items). That edge is
567                // added by `visit_layout_items_layoutinfo_cross_axis_dependencies`
568                // so the binding-loop pass can detect the cycle.
569                use crate::layout::FlexboxAxisRelation;
570                match layout.axis_relation(Orientation::Horizontal) {
571                    FlexboxAxisRelation::MainAxis => {
572                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
573                            vis(&nr.clone().into(), P);
574                        }
575                        visit_layout_items_layoutinfo_cross_axis_dependencies(
576                            layout.elems.iter().map(|fi| &fi.item),
577                            Orientation::Vertical,
578                            vis,
579                        );
580                    }
581                    FlexboxAxisRelation::CrossAxis => {
582                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
583                            vis(&nr.clone().into(), P);
584                        }
585                        visit_layout_items_layoutinfo_cross_axis_dependencies(
586                            layout.elems.iter().map(|fi| &fi.item),
587                            Orientation::Horizontal,
588                            vis,
589                        );
590                    }
591                    FlexboxAxisRelation::Unknown => {
592                        // Runtime direction: conservatively depend on both
593                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
594                            vis(&nr.clone().into(), P);
595                        }
596                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
597                            vis(&nr.clone().into(), P);
598                        }
599                        visit_layout_items_layoutinfo_cross_axis_dependencies(
600                            layout.elems.iter().map(|fi| &fi.item),
601                            Orientation::Horizontal,
602                            vis,
603                        );
604                        visit_layout_items_layoutinfo_cross_axis_dependencies(
605                            layout.elems.iter().map(|fi| &fi.item),
606                            Orientation::Vertical,
607                            vis,
608                        );
609                    }
610                }
611            } else if let Expression::ComputeFlexboxLayoutInfo(_, orientation) = expr {
612                use crate::layout::FlexboxAxisRelation;
613                match layout.axis_relation(*orientation) {
614                    FlexboxAxisRelation::MainAxis => {
615                        // Main axis: only visit same-axis item dependencies
616                        visit_layout_items_dependencies(
617                            layout.elems.iter().map(|fi| &fi.item),
618                            *orientation,
619                            vis,
620                        );
621                    }
622                    FlexboxAxisRelation::CrossAxis => {
623                        // Cross axis: depends on the perpendicular (main-axis) dimension
624                        // for accurate wrapping.
625                        if *orientation == Orientation::Vertical
626                            && let Some(nr) = layout.geometry.rect.width_reference.as_ref()
627                        {
628                            vis(&nr.clone().into(), P);
629                        }
630                        if *orientation == Orientation::Horizontal
631                            && let Some(nr) = layout.geometry.rect.height_reference.as_ref()
632                        {
633                            vis(&nr.clone().into(), P);
634                        }
635                        visit_layout_items_dependencies(
636                            layout.elems.iter().map(|fi| &fi.item),
637                            Orientation::Horizontal,
638                            vis,
639                        );
640                        visit_layout_items_dependencies(
641                            layout.elems.iter().map(|fi| &fi.item),
642                            Orientation::Vertical,
643                            vis,
644                        );
645                    }
646                    FlexboxAxisRelation::Unknown => {
647                        // Unknown direction: visit both orientations' item
648                        // dependencies but NOT perpendicular dimensions (adding
649                        // those leads to binding loops for runtime direction).
650                        visit_layout_items_dependencies(
651                            layout.elems.iter().map(|fi| &fi.item),
652                            Orientation::Horizontal,
653                            vis,
654                        );
655                        visit_layout_items_dependencies(
656                            layout.elems.iter().map(|fi| &fi.item),
657                            Orientation::Vertical,
658                            vis,
659                        );
660                    }
661                }
662            }
663            let mut g = layout.geometry.clone();
664            g.rect = Default::default(); // already visited;
665            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
666        }
667        Expression::OrganizeGridLayout(layout) => {
668            let mut layout = layout.clone();
669            layout.visit_rowcol_named_references(&mut |nr: &mut NamedReference| {
670                vis(&nr.clone().into(), P)
671            });
672        }
673        Expression::SolveGridLayout { layout_organized_data_prop, layout, orientation }
674        | Expression::ComputeGridLayoutInfo { layout_organized_data_prop, layout, orientation } => {
675            // we should only visit the layout geometry for the orientation
676            if matches!(expr, Expression::SolveGridLayout { .. })
677                && let Some(nr) = layout.geometry.rect.size_reference(*orientation)
678            {
679                vis(&nr.clone().into(), P);
680            }
681            vis(&layout_organized_data_prop.clone().into(), P);
682            visit_layout_items_dependencies(
683                layout.elems.iter().map(|it| &it.item),
684                *orientation,
685                vis,
686            );
687            let mut g = layout.geometry.clone();
688            g.rect = Default::default(); // already visited;
689            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
690        }
691        Expression::FunctionCall {
692            function: Callable::Callback(nr) | Callable::Function(nr),
693            ..
694        } => vis(&nr.clone().into(), P),
695        Expression::FunctionCall { function: Callable::Builtin(b), arguments, .. } => match b {
696            BuiltinFunction::ImplicitLayoutInfo(orientation) => {
697                if let [Expression::ElementReference(item), ..] = arguments.as_slice() {
698                    visit_implicit_layout_info_dependencies(
699                        *orientation,
700                        &item.upgrade().unwrap(),
701                        vis,
702                    );
703                }
704            }
705            BuiltinFunction::ItemAbsolutePosition => {
706                if let Some(Expression::ElementReference(item)) = arguments.first() {
707                    let mut item = item.upgrade().unwrap();
708                    while let Some(parent) = find_parent_element(&item) {
709                        item = parent;
710                        vis(
711                            &NamedReference::new(&item, SmolStr::new_static("x")).into(),
712                            ReadType::NativeRead,
713                        );
714                        vis(
715                            &NamedReference::new(&item, SmolStr::new_static("y")).into(),
716                            ReadType::NativeRead,
717                        );
718                    }
719                }
720            }
721            BuiltinFunction::ItemFontMetrics => {
722                if let Some(Expression::ElementReference(item)) = arguments.first() {
723                    let item = item.upgrade().unwrap();
724                    vis(
725                        &NamedReference::new(&item, SmolStr::new_static("font-size")).into(),
726                        ReadType::NativeRead,
727                    );
728                    vis(
729                        &NamedReference::new(&item, SmolStr::new_static("font-weight")).into(),
730                        ReadType::NativeRead,
731                    );
732                    vis(
733                        &NamedReference::new(&item, SmolStr::new_static("font-family")).into(),
734                        ReadType::NativeRead,
735                    );
736                    vis(
737                        &NamedReference::new(&item, SmolStr::new_static("font-italic")).into(),
738                        ReadType::NativeRead,
739                    );
740                }
741            }
742            BuiltinFunction::GetWindowDefaultFontSize => {
743                let root =
744                    elem.borrow().enclosing_component.upgrade().unwrap().root_element.clone();
745                if root.borrow().builtin_type().is_some_and(|bt| bt.name == "Window") {
746                    vis(
747                        &NamedReference::new(&root, SmolStr::new_static("default-font-size"))
748                            .into(),
749                        ReadType::PropertyRead,
750                    );
751                }
752            }
753            _ => {}
754        },
755        _ => {}
756    }
757}
758
759fn visit_layout_items_dependencies<'a>(
760    items: impl Iterator<Item = &'a LayoutItem>,
761    orientation: Orientation,
762    vis: &mut impl FnMut(&PropertyPath, ReadType),
763) {
764    for it in items {
765        let mut element = it.element.clone();
766        if element
767            .borrow()
768            .repeated
769            .as_ref()
770            .map(|r| recurse_expression(&element, &r.model, vis))
771            .is_some()
772        {
773            element = it.element.borrow().base_type.as_component().root_element.clone();
774        }
775
776        if let Some(nr) = element.borrow().layout_info_prop(orientation) {
777            vis(&nr.clone().into(), ReadType::PropertyRead);
778        } else {
779            if let ElementType::Component(base) = &element.borrow().base_type
780                && let Some(nr) = base.root_element.borrow().layout_info_prop(orientation)
781            {
782                vis(
783                    &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
784                    ReadType::PropertyRead,
785                );
786            }
787            visit_implicit_layout_info_dependencies(orientation, &element, vis);
788        }
789
790        for (nr, _) in it.constraints.for_each_restrictions(orientation) {
791            vis(&nr.clone().into(), ReadType::PropertyRead)
792        }
793    }
794}
795
796/// Visit cross-axis `layoutinfo-<cross>` dependencies for child elements that
797/// have a compiled `layoutinfo-<cross>` binding (i.e. an inlined component
798/// root, or a nested layout).
799///
800/// Pure builtins (`Image`, `Text`, `Rectangle`, …) do not set `layout_info_prop`
801/// — their cross-axis size is computed through the item VTable, which accepts a
802/// `cross_axis_constraint` argument, so they never read `self.width` at
803/// runtime and the parent's `SolveFlexboxLayout` has no real dependency on
804/// them. Elements that *do* set `layout_info_prop` run an ordinary property
805/// binding that may transitively depend on the cross-axis dimension (e.g. an
806/// inner word-wrapping `Text` or aspect-ratio `Image`). Declaring that edge
807/// lets `binding_analysis` detect cycles through component boundaries instead
808/// of letting them surface as a runtime recursion panic.
809fn visit_layout_items_layoutinfo_cross_axis_dependencies<'a>(
810    items: impl Iterator<Item = &'a LayoutItem>,
811    cross_axis: Orientation,
812    vis: &mut impl FnMut(&PropertyPath, ReadType),
813) {
814    for it in items {
815        let element = it.element.clone();
816        if let Some(nr) = element.borrow().layout_info_prop(cross_axis) {
817            vis(&nr.clone().into(), ReadType::PropertyRead);
818        } else if let ElementType::Component(base) = &element.borrow().base_type
819            && let Some(nr) = base.root_element.borrow().layout_info_prop(cross_axis)
820        {
821            vis(
822                &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
823                ReadType::PropertyRead,
824            );
825        }
826    }
827}
828
829/// The builtin function can call native code, and we need to visit the properties that are accessed by it
830fn visit_implicit_layout_info_dependencies(
831    orientation: crate::layout::Orientation,
832    item: &ElementRc,
833    vis: &mut impl FnMut(&PropertyPath, ReadType),
834) {
835    let base_type = item.borrow().base_type.to_smolstr();
836    const N: ReadType = ReadType::NativeRead;
837    match base_type.as_str() {
838        "Image" => {
839            vis(&NamedReference::new(item, SmolStr::new_static("source")).into(), N);
840            vis(&NamedReference::new(item, SmolStr::new_static("source-clip-width")).into(), N);
841            if orientation == Orientation::Vertical {
842                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
843                vis(
844                    &NamedReference::new(item, SmolStr::new_static("source-clip-height")).into(),
845                    N,
846                );
847            }
848        }
849        "Text" | "TextInput" => {
850            vis(&NamedReference::new(item, SmolStr::new_static("text")).into(), N);
851            vis(&NamedReference::new(item, SmolStr::new_static("font-family")).into(), N);
852            vis(&NamedReference::new(item, SmolStr::new_static("font-size")).into(), N);
853            vis(&NamedReference::new(item, SmolStr::new_static("font-weight")).into(), N);
854            vis(&NamedReference::new(item, SmolStr::new_static("letter-spacing")).into(), N);
855            vis(&NamedReference::new(item, SmolStr::new_static("wrap")).into(), N);
856            let wrap_set = item.borrow().is_binding_set("wrap", false)
857                || item
858                    .borrow()
859                    .property_analysis
860                    .borrow()
861                    .get("wrap")
862                    .is_some_and(|a| a.is_set || a.is_set_externally);
863            if wrap_set && orientation == Orientation::Vertical {
864                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
865            }
866            if base_type.as_str() == "TextInput" {
867                vis(&NamedReference::new(item, SmolStr::new_static("single-line")).into(), N);
868            } else {
869                vis(&NamedReference::new(item, SmolStr::new_static("overflow")).into(), N);
870            }
871        }
872
873        _ => (),
874    }
875}
876
877fn visit_builtin_property(
878    builtin: &crate::langtype::BuiltinElement,
879    prop: &PropertyPath,
880    context: &mut AnalysisContext,
881    reverse_aliases: &ReverseAliases,
882    diag: &mut BuildDiagnostics,
883) {
884    let name = prop.prop.name();
885    if builtin.name == "Window" {
886        for (p, orientation) in
887            [("width", Orientation::Horizontal), ("height", Orientation::Vertical)]
888        {
889            if name == p {
890                // find the actual root component
891                let is_root = |e: &ElementRc| -> bool {
892                    ElementRc::ptr_eq(
893                        e,
894                        &e.borrow().enclosing_component.upgrade().unwrap().root_element,
895                    )
896                };
897                let mut root = prop.prop.element();
898                if !is_root(&root) {
899                    return;
900                };
901                for e in prop.elements.iter().rev() {
902                    if !is_root(&e.0) {
903                        return;
904                    }
905                    root = e.0.clone();
906                }
907                if let Some(p) = root.borrow().layout_info_prop(orientation) {
908                    let path = PropertyPath::from(p.clone());
909                    let old_layout = context.window_layout_property.replace(path.clone());
910                    process_property(&path, ReadType::NativeRead, context, reverse_aliases, diag);
911                    context.window_layout_property = old_layout;
912                };
913            }
914        }
915    }
916}
917
918/// Analyze the Window default-font-size property
919fn check_window_properties(doc: &Document, global_analysis: &mut GlobalAnalysis) {
920    doc.visit_all_used_components(|component| {
921        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
922            component,
923            &(),
924            &mut |elem, _| {
925                if elem.borrow().builtin_type().as_ref().is_some_and(|b| b.name == "Window") {
926                    const DEFAULT_FONT_SIZE: &str = "default-font-size";
927                    if elem.borrow().is_binding_set(DEFAULT_FONT_SIZE, false)
928                        || elem
929                            .borrow()
930                            .property_analysis
931                            .borrow()
932                            .get(DEFAULT_FONT_SIZE)
933                            .is_some_and(|a| a.is_set)
934                    {
935                        let value = elem.borrow().bindings.get(DEFAULT_FONT_SIZE).and_then(|e| {
936                            match &e.borrow().expression {
937                                Expression::NumberLiteral(v, crate::expression_tree::Unit::Px) => {
938                                    Some(*v as f32)
939                                }
940                                _ => None,
941                            }
942                        });
943                        let is_const = value.is_some()
944                            || NamedReference::new(elem, SmolStr::new_static(DEFAULT_FONT_SIZE))
945                                .is_constant();
946                        global_analysis.default_font_size = match global_analysis.default_font_size
947                        {
948                            DefaultFontSize::Unknown => match value {
949                                Some(v) => DefaultFontSize::LogicalValue(v),
950                                None if is_const => DefaultFontSize::Const,
951                                None => DefaultFontSize::Variable,
952                            },
953                            DefaultFontSize::NotSet if is_const => DefaultFontSize::NotSet,
954                            DefaultFontSize::LogicalValue(val) => match value {
955                                Some(v) if v == val => DefaultFontSize::LogicalValue(val),
956                                _ if is_const => DefaultFontSize::Const,
957                                _ => DefaultFontSize::Variable,
958                            },
959                            DefaultFontSize::Const if is_const => DefaultFontSize::Const,
960                            _ => DefaultFontSize::Variable,
961                        }
962                    } else {
963                        global_analysis.default_font_size = match global_analysis.default_font_size
964                        {
965                            DefaultFontSize::Unknown => DefaultFontSize::NotSet,
966                            DefaultFontSize::NotSet => DefaultFontSize::NotSet,
967                            DefaultFontSize::LogicalValue(_) => DefaultFontSize::NotSet,
968                            DefaultFontSize::Const => DefaultFontSize::NotSet,
969                            DefaultFontSize::Variable => DefaultFontSize::Variable,
970                        }
971                    }
972                }
973            },
974        );
975    });
976}
977
978/// Make sure that the is_set property analysis is set to any property which has a two way binding
979/// to a property that is, itself, is set
980///
981/// Example:
982/// ```slint
983/// Xx := TouchArea {
984///    property <int> bar <=> foo;
985///    clicked => { bar+=1; }
986///    property <int> foo; // must ensure that this is not considered as const, because the alias with bar
987/// }
988/// ```
989fn propagate_is_set_on_aliases(doc: &Document, reverse_aliases: &mut ReverseAliases) {
990    doc.visit_all_used_components(|component| {
991        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
992            component,
993            &(),
994            &mut |e, _| visit_element(e, reverse_aliases),
995        );
996    });
997
998    fn visit_element(e: &ElementRc, reverse_aliases: &mut ReverseAliases) {
999        for (name, binding) in &e.borrow().bindings {
1000            if !binding.borrow().two_way_bindings.is_empty() {
1001                check_alias(e, name, &binding.borrow());
1002
1003                let nr = NamedReference::new(e, name.clone());
1004                for a in &binding.borrow().two_way_bindings {
1005                    if a.property != nr
1006                        && !a
1007                            .property
1008                            .element()
1009                            .borrow()
1010                            .enclosing_component
1011                            .upgrade()
1012                            .unwrap()
1013                            .is_global()
1014                    {
1015                        reverse_aliases.entry(a.property.clone()).or_default().push(nr.clone())
1016                    }
1017                }
1018            }
1019        }
1020        for decl in e.borrow().property_declarations.values() {
1021            if let Some(alias) = &decl.is_alias {
1022                mark_alias(alias)
1023            }
1024        }
1025    }
1026
1027    fn check_alias(e: &ElementRc, name: &SmolStr, binding: &BindingExpression) {
1028        // Note: since the analysis hasn't been run, any property access will result in a non constant binding. this is slightly non-optimal
1029        let is_binding_constant = binding.is_constant(None)
1030            && binding.two_way_bindings.iter().all(|n| n.property.is_constant());
1031        if is_binding_constant && !NamedReference::new(e, name.clone()).is_externally_modified() {
1032            for alias in &binding.two_way_bindings {
1033                crate::namedreference::mark_property_set_derived_in_base(
1034                    alias.property.element(),
1035                    alias.property.name(),
1036                );
1037            }
1038            return;
1039        }
1040
1041        propagate_alias(binding);
1042    }
1043
1044    fn propagate_alias(binding: &BindingExpression) {
1045        for alias in &binding.two_way_bindings {
1046            mark_alias(&alias.property);
1047        }
1048    }
1049
1050    fn mark_alias(alias: &NamedReference) {
1051        alias.mark_as_set();
1052        if !alias.is_externally_modified()
1053            && let Some(bind) = alias.element().borrow().bindings.get(alias.name())
1054        {
1055            propagate_alias(&bind.borrow())
1056        }
1057    }
1058}
1059
1060/// Make sure that the is_set_externally is true for all bindings.
1061/// And change bindings are used externally
1062fn mark_used_base_properties(doc: &Document) {
1063    doc.visit_all_used_components(|component| {
1064        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1065            component,
1066            &(),
1067            &mut |element, _| {
1068                if !matches!(element.borrow().base_type, ElementType::Component(_)) {
1069                    return;
1070                }
1071                for (name, binding) in &element.borrow().bindings {
1072                    if binding.borrow().has_binding() {
1073                        crate::namedreference::mark_property_set_derived_in_base(
1074                            element.clone(),
1075                            name,
1076                        );
1077                    }
1078                }
1079                for name in element.borrow().change_callbacks.keys() {
1080                    crate::namedreference::mark_property_read_derived_in_base(
1081                        element.clone(),
1082                        name,
1083                    );
1084                }
1085            },
1086        );
1087    });
1088}