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 let Some(p) = twb.property()
386            && p != &current.prop
387        {
388            depends_on_external |= process_property(
389                &current.relative(&p.clone().into()),
390                ReadType::PropertyRead,
391                context,
392                reverse_aliases,
393                diag,
394            );
395        }
396    }
397
398    let mut process_prop = |prop: &PropertyPath, r, context: &mut AnalysisContext| {
399        depends_on_external |=
400            process_property(&current.relative(prop), r, context, reverse_aliases, diag);
401        for x in reverse_aliases.get(&prop.prop).unwrap_or(&Default::default()) {
402            if x != &current.prop && x != &prop.prop {
403                depends_on_external |= process_property(
404                    &current.relative(&x.clone().into()),
405                    ReadType::PropertyRead,
406                    context,
407                    reverse_aliases,
408                    diag,
409                );
410            }
411        }
412    };
413
414    recurse_expression(&current.prop.element(), &b.expression, &mut |p, r| {
415        process_prop(p, r, context)
416    });
417
418    let mut is_const = b.expression.is_constant(Some(context.global_analysis))
419        && b.two_way_bindings.iter().all(|n| n.is_constant());
420
421    if is_const && matches!(b.expression, Expression::Invalid) {
422        // check the base
423        if let Some(base) = element.borrow().sub_component() {
424            is_const = NamedReference::new(&base.root_element, name.clone()).is_constant();
425        }
426    }
427    drop(b);
428
429    if let Ok(mut b) = binding.try_borrow_mut() {
430        // We have a loop (through different component so we're still borrowed)
431        b.analysis.as_mut().unwrap().is_const = is_const;
432    }
433
434    match &binding.borrow().animation {
435        Some(PropertyAnimation::Static(e)) => analyze_element(e, context, reverse_aliases, diag),
436        Some(PropertyAnimation::Transition { animations, state_ref }) => {
437            recurse_expression(&current.prop.element(), state_ref, &mut |p, r| {
438                process_prop(p, r, context)
439            });
440            for a in animations {
441                analyze_element(&a.animation, context, reverse_aliases, diag);
442            }
443        }
444        None => (),
445    }
446
447    let o = context.currently_analyzing.pop_back();
448    assert_eq!(&o.unwrap(), current);
449
450    depends_on_external
451}
452
453#[derive(Copy, Clone, Eq, PartialEq)]
454enum ReadType {
455    // Read from the native code
456    NativeRead,
457    // Read from another property binding in Slint
458    PropertyRead,
459}
460
461/// Process the property `prop`
462///
463/// This will visit all the bindings from that property
464fn process_property(
465    prop: &PropertyPath,
466    read_type: ReadType,
467    context: &mut AnalysisContext,
468    reverse_aliases: &ReverseAliases,
469    diag: &mut BuildDiagnostics,
470) -> DependsOnExternal {
471    #[allow(clippy::match_single_binding)]
472    let depends_on_external = match prop
473        .prop
474        .element()
475        .borrow()
476        .property_analysis
477        .borrow_mut()
478        .entry(prop.prop.name().clone())
479        .or_default()
480    {
481        a => {
482            if read_type == ReadType::PropertyRead {
483                a.is_read = true;
484            }
485            DependsOnExternal(prop.elements.is_empty() && a.is_set_externally)
486        }
487    };
488
489    let mut prop = prop.clone();
490
491    loop {
492        let element = prop.prop.element();
493        if element.borrow().bindings.contains_key(prop.prop.name()) {
494            analyze_binding(&prop, context, reverse_aliases, diag);
495            break;
496        }
497        let next = match &element.borrow().base_type {
498            ElementType::Component(base) => {
499                if element.borrow().property_declarations.contains_key(prop.prop.name()) {
500                    break;
501                }
502                base.root_element.clone()
503            }
504            ElementType::Builtin(builtin) => {
505                if builtin.properties.contains_key(prop.prop.name()) {
506                    visit_builtin_property(builtin, &prop, context, reverse_aliases, diag);
507                }
508                break;
509            }
510            _ => break,
511        };
512        next.borrow()
513            .property_analysis
514            .borrow_mut()
515            .entry(prop.prop.name().clone())
516            .or_default()
517            .is_read_externally = true;
518        prop.elements.push(element.into());
519        prop.prop = NamedReference::new(&next, prop.prop.name().clone());
520    }
521    depends_on_external
522}
523
524// Same as in crate::visit_all_named_references_in_element, but not mut
525fn recurse_expression(
526    elem: &ElementRc,
527    expr: &Expression,
528    vis: &mut impl FnMut(&PropertyPath, ReadType),
529) {
530    const P: ReadType = ReadType::PropertyRead;
531    expr.visit(|sub| recurse_expression(elem, sub, vis));
532    match expr {
533        Expression::PropertyReference(r) => vis(&r.clone().into(), P),
534        Expression::LayoutCacheAccess { layout_cache_prop, .. } => {
535            vis(&layout_cache_prop.clone().into(), P)
536        }
537        Expression::GridRepeaterCacheAccess { layout_cache_prop, .. } => {
538            vis(&layout_cache_prop.clone().into(), P)
539        }
540        Expression::SolveBoxLayout(l, o)
541        | Expression::ComputeBoxLayoutInfo { layout: l, orientation: o, .. } => {
542            // we should only visit the layout geometry for the orientation
543            if matches!(expr, Expression::SolveBoxLayout(..))
544                && let Some(nr) = l.geometry.rect.size_reference(*o)
545            {
546                vis(&nr.clone().into(), P);
547            }
548            visit_layout_items_dependencies(l.elems.iter(), *o, vis);
549
550            // The orthogonal solve depends on `cross-axis-alignment`.
551            if matches!(expr, Expression::SolveBoxLayout(..))
552                && *o != l.orientation
553                && let Some(nr) = l.cross_alignment.as_ref()
554            {
555                vis(&nr.clone().into(), P);
556            }
557
558            let mut g = l.geometry.clone();
559            g.rect = Default::default(); // already visited;
560            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
561        }
562        Expression::SolveFlexboxLayout(layout)
563        | Expression::ComputeFlexboxLayoutInfo { layout, .. } => {
564            if let Some(nr) = layout.direction.as_ref() {
565                vis(&nr.clone().into(), P);
566            }
567            // Visit layout geometry dependencies
568            if matches!(expr, Expression::SolveFlexboxLayout(..)) {
569                // The solve needs the main-axis dimension (width for row,
570                // height for column). On the cross axis, *builtin* items
571                // receive the perpendicular size through the item VTable's
572                // `cross_axis_constraint` parameter and never read `self.width`
573                // at runtime, so no edge is declared for them here. *Component*
574                // items don't have that shortcut: their compiled
575                // `layoutinfo-<cross>` binding is evaluated as an ordinary
576                // property and really does depend on the cross-axis dimension
577                // (typically via inner height-for-width items). That edge is
578                // added by `visit_layout_items_layoutinfo_cross_axis_dependencies`
579                // so the binding-loop pass can detect the cycle.
580                use crate::layout::FlexboxAxisRelation;
581                match layout.axis_relation(Orientation::Horizontal) {
582                    FlexboxAxisRelation::MainAxis => {
583                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
584                            vis(&nr.clone().into(), P);
585                        }
586                        visit_layout_items_layoutinfo_cross_axis_dependencies(
587                            layout.elems.iter().map(|fi| &fi.item),
588                            Orientation::Vertical,
589                            vis,
590                        );
591                    }
592                    FlexboxAxisRelation::CrossAxis => {
593                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
594                            vis(&nr.clone().into(), P);
595                        }
596                        visit_layout_items_layoutinfo_cross_axis_dependencies(
597                            layout.elems.iter().map(|fi| &fi.item),
598                            Orientation::Horizontal,
599                            vis,
600                        );
601                    }
602                    FlexboxAxisRelation::Unknown => {
603                        // Runtime direction: conservatively depend on both
604                        if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
605                            vis(&nr.clone().into(), P);
606                        }
607                        if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
608                            vis(&nr.clone().into(), P);
609                        }
610                        visit_layout_items_layoutinfo_cross_axis_dependencies(
611                            layout.elems.iter().map(|fi| &fi.item),
612                            Orientation::Horizontal,
613                            vis,
614                        );
615                        visit_layout_items_layoutinfo_cross_axis_dependencies(
616                            layout.elems.iter().map(|fi| &fi.item),
617                            Orientation::Vertical,
618                            vis,
619                        );
620                    }
621                }
622            } else if let Expression::ComputeFlexboxLayoutInfo { orientation, .. } = expr {
623                let orientation = *orientation;
624                use crate::layout::FlexboxAxisRelation;
625                match layout.axis_relation(orientation) {
626                    FlexboxAxisRelation::MainAxis => {
627                        // Main axis: only visit same-axis item dependencies
628                        visit_layout_items_dependencies(
629                            layout.elems.iter().map(|fi| &fi.item),
630                            orientation,
631                            vis,
632                        );
633                    }
634                    FlexboxAxisRelation::CrossAxis => {
635                        // Cross axis: depends on the perpendicular (main-axis)
636                        // dimension for accurate wrapping. Skip that edge
637                        // when the element has a parametrized layout-info
638                        // function — callers that would otherwise cycle go
639                        // through it instead, so the bare binding's read of
640                        // `self.{w,h}` is a fallback only.
641                        if orientation == Orientation::Vertical
642                            && let Some(nr) = layout.geometry.rect.width_reference.as_ref()
643                            && nr.element().borrow().layout_info_v_with_constraint.is_none()
644                        {
645                            vis(&nr.clone().into(), P);
646                        }
647                        if orientation == Orientation::Horizontal
648                            && let Some(nr) = layout.geometry.rect.height_reference.as_ref()
649                            && nr.element().borrow().layout_info_h_with_constraint.is_none()
650                        {
651                            vis(&nr.clone().into(), P);
652                        }
653                        visit_layout_items_dependencies(
654                            layout.elems.iter().map(|fi| &fi.item),
655                            Orientation::Horizontal,
656                            vis,
657                        );
658                        visit_layout_items_dependencies(
659                            layout.elems.iter().map(|fi| &fi.item),
660                            Orientation::Vertical,
661                            vis,
662                        );
663                    }
664                    FlexboxAxisRelation::Unknown => {
665                        // Unknown direction: visit both orientations' item
666                        // dependencies but NOT perpendicular dimensions (adding
667                        // those leads to binding loops for runtime direction).
668                        visit_layout_items_dependencies(
669                            layout.elems.iter().map(|fi| &fi.item),
670                            Orientation::Horizontal,
671                            vis,
672                        );
673                        visit_layout_items_dependencies(
674                            layout.elems.iter().map(|fi| &fi.item),
675                            Orientation::Vertical,
676                            vis,
677                        );
678                    }
679                }
680            }
681            let mut g = layout.geometry.clone();
682            g.rect = Default::default(); // already visited;
683            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
684        }
685        Expression::OrganizeGridLayout(layout) => {
686            let mut layout = layout.clone();
687            layout.visit_rowcol_named_references(&mut |nr: &mut NamedReference| {
688                vis(&nr.clone().into(), P)
689            });
690        }
691        Expression::SolveGridLayout { layout_organized_data_prop, layout, orientation }
692        | Expression::ComputeGridLayoutInfo {
693            layout_organized_data_prop,
694            layout,
695            orientation,
696            ..
697        } => {
698            // we should only visit the layout geometry for the orientation
699            if matches!(expr, Expression::SolveGridLayout { .. })
700                && let Some(nr) = layout.geometry.rect.size_reference(*orientation)
701            {
702                vis(&nr.clone().into(), P);
703            }
704            vis(&layout_organized_data_prop.clone().into(), P);
705            visit_layout_items_dependencies(
706                layout.elems.iter().map(|it| &it.item),
707                *orientation,
708                vis,
709            );
710            let mut g = layout.geometry.clone();
711            g.rect = Default::default(); // already visited;
712            g.visit_named_references(&mut |nr| vis(&nr.clone().into(), P))
713        }
714        Expression::FunctionCall {
715            function: Callable::Callback(nr) | Callable::Function(nr),
716            ..
717        } => vis(&nr.clone().into(), P),
718        Expression::FunctionCall { function: Callable::Builtin(b), arguments, .. } => match b {
719            BuiltinFunction::ImplicitLayoutInfo(orientation) => {
720                if let [Expression::ElementReference(item), ..] = arguments.as_slice() {
721                    visit_implicit_layout_info_dependencies(
722                        *orientation,
723                        &item.upgrade().unwrap(),
724                        vis,
725                    );
726                }
727            }
728            BuiltinFunction::ItemAbsolutePosition => {
729                if let Some(Expression::ElementReference(item)) = arguments.first() {
730                    let mut item = item.upgrade().unwrap();
731                    while let Some(parent) = find_parent_element(&item) {
732                        item = parent;
733                        vis(
734                            &NamedReference::new(&item, SmolStr::new_static("x")).into(),
735                            ReadType::NativeRead,
736                        );
737                        vis(
738                            &NamedReference::new(&item, SmolStr::new_static("y")).into(),
739                            ReadType::NativeRead,
740                        );
741                    }
742                }
743            }
744            BuiltinFunction::ItemFontMetrics => {
745                if let Some(Expression::ElementReference(item)) = arguments.first() {
746                    let item = item.upgrade().unwrap();
747                    vis(
748                        &NamedReference::new(&item, SmolStr::new_static("font-size")).into(),
749                        ReadType::NativeRead,
750                    );
751                    vis(
752                        &NamedReference::new(&item, SmolStr::new_static("font-weight")).into(),
753                        ReadType::NativeRead,
754                    );
755                    vis(
756                        &NamedReference::new(&item, SmolStr::new_static("font-family")).into(),
757                        ReadType::NativeRead,
758                    );
759                    vis(
760                        &NamedReference::new(&item, SmolStr::new_static("font-italic")).into(),
761                        ReadType::NativeRead,
762                    );
763                }
764            }
765            BuiltinFunction::GetWindowDefaultFontSize => {
766                let root =
767                    elem.borrow().enclosing_component.upgrade().unwrap().root_element.clone();
768                if root.borrow().builtin_type().is_some_and(|bt| bt.name == "Window") {
769                    vis(
770                        &NamedReference::new(&root, SmolStr::new_static("default-font-size"))
771                            .into(),
772                        ReadType::PropertyRead,
773                    );
774                }
775            }
776            _ => {}
777        },
778        _ => {}
779    }
780}
781
782fn visit_layout_items_dependencies<'a>(
783    items: impl Iterator<Item = &'a LayoutItem>,
784    orientation: Orientation,
785    vis: &mut impl FnMut(&PropertyPath, ReadType),
786) {
787    for it in items {
788        let mut element = it.element.clone();
789        if element
790            .borrow()
791            .repeated
792            .as_ref()
793            .map(|r| recurse_expression(&element, &r.model, vis))
794            .is_some()
795        {
796            element = it.element.borrow().base_type.as_component().root_element.clone();
797        }
798
799        if let Some(nr) = element.borrow().layout_info_prop(orientation) {
800            vis(&nr.clone().into(), ReadType::PropertyRead);
801        } else {
802            if let ElementType::Component(base) = &element.borrow().base_type
803                && let Some(nr) = base.root_element.borrow().layout_info_prop(orientation)
804            {
805                vis(
806                    &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
807                    ReadType::PropertyRead,
808                );
809            }
810            visit_implicit_layout_info_dependencies(orientation, &element, vis);
811        }
812
813        for (nr, _) in it.constraints.for_each_restrictions(orientation) {
814            vis(&nr.clone().into(), ReadType::PropertyRead)
815        }
816    }
817}
818
819/// Visit cross-axis `layoutinfo-<cross>` dependencies for child elements that
820/// have a compiled `layoutinfo-<cross>` binding (i.e. an inlined component
821/// root, or a nested layout) and no parametrized variant that bypasses it.
822///
823/// Pure builtins (`Image`, `Text`, `Rectangle`, …) do not set `layout_info_prop`
824/// — their cross-axis size is computed through the item VTable, which accepts a
825/// `cross_axis_constraint` argument, so they never read `self.{w,h}` at
826/// runtime and the parent's `SolveFlexboxLayout` has no real dependency on
827/// them. Elements that *do* set `layout_info_prop` run an ordinary property
828/// binding that may transitively depend on the cross-axis dimension.
829/// `implicit_layout_info_call` dispatches via the parametrized
830/// `layoutinfo-{v,h}-with-constraint` function when the child carries one, so
831/// the property dependency only exists at runtime for cells without that
832/// function — mirror that here.
833fn visit_layout_items_layoutinfo_cross_axis_dependencies<'a>(
834    items: impl Iterator<Item = &'a LayoutItem>,
835    cross_axis: Orientation,
836    vis: &mut impl FnMut(&PropertyPath, ReadType),
837) {
838    for it in items {
839        let element = it.element.clone();
840        // Parent dispatches via the parametrized function, not the property.
841        let bypassed = match cross_axis {
842            Orientation::Vertical => {
843                element.borrow().inherited_layout_info_v_with_constraint().is_some()
844            }
845            Orientation::Horizontal => {
846                element.borrow().inherited_layout_info_h_with_constraint().is_some()
847            }
848        };
849        if bypassed {
850            continue;
851        }
852        if let Some(nr) = element.borrow().layout_info_prop(cross_axis) {
853            vis(&nr.clone().into(), ReadType::PropertyRead);
854        } else if let ElementType::Component(base) = &element.borrow().base_type
855            && let Some(nr) = base.root_element.borrow().layout_info_prop(cross_axis)
856        {
857            vis(
858                &PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
859                ReadType::PropertyRead,
860            );
861        } else {
862            visit_cell_cross_axis_implicit_dependency(cross_axis, &element, vis);
863        }
864    }
865}
866
867/// Cross-axis variant of [`visit_implicit_layout_info_dependencies`]: only
868/// declare deps that actually exist on the cross-axis path. Image/Text and
869/// other h-for-w builtins receive the cross-axis size via the item VTable's
870/// `cross_axis_constraint`, so they don't read `self.{w,h}` here. For other
871/// items (user components, plain builtins), the native `ImplicitLayoutInfo`
872/// reads `preferred-{w,h}` — declare it when the user has bound it to read
873/// the opposite-axis dim on the same element. That catches cycles like
874/// `preferred-height: self.width` at compile time instead of panicking at
875/// runtime.
876fn visit_cell_cross_axis_implicit_dependency(
877    cross_axis: Orientation,
878    item: &ElementRc,
879    vis: &mut impl FnMut(&PropertyPath, ReadType),
880) {
881    let base_type = item.borrow().base_type.to_smolstr();
882    if matches!(base_type.as_str(), "Image" | "ClippedImage" | "Text" | "TextInput" | "StyledText")
883    {
884        return;
885    }
886    let (prop, opposite_dim) = match cross_axis {
887        Orientation::Horizontal => ("preferred-width", "height"),
888        Orientation::Vertical => ("preferred-height", "width"),
889    };
890    if !item.borrow().is_binding_set(prop, false) {
891        return;
892    }
893    let reads_opposite = item
894        .borrow()
895        .bindings
896        .get(prop)
897        .map(|b| {
898            let mut seen = false;
899            b.borrow().expression.visit_recursive(&mut |sub| {
900                if let Expression::PropertyReference(nr) = sub
901                    && nr.name() == opposite_dim
902                    && Rc::ptr_eq(&nr.element(), item)
903                {
904                    seen = true;
905                }
906            });
907            seen
908        })
909        .unwrap_or(false);
910    if reads_opposite {
911        vis(&NamedReference::new(item, SmolStr::new_static(prop)).into(), ReadType::NativeRead);
912    }
913}
914
915/// The builtin function can call native code, and we need to visit the properties that are accessed by it
916fn visit_implicit_layout_info_dependencies(
917    orientation: crate::layout::Orientation,
918    item: &ElementRc,
919    vis: &mut impl FnMut(&PropertyPath, ReadType),
920) {
921    let base_type = item.borrow().base_type.to_smolstr();
922    const N: ReadType = ReadType::NativeRead;
923    match base_type.as_str() {
924        "Image" => {
925            vis(&NamedReference::new(item, SmolStr::new_static("source")).into(), N);
926            vis(&NamedReference::new(item, SmolStr::new_static("source-clip-width")).into(), N);
927            if orientation == Orientation::Vertical {
928                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
929                vis(
930                    &NamedReference::new(item, SmolStr::new_static("source-clip-height")).into(),
931                    N,
932                );
933            }
934        }
935        "Text" | "TextInput" => {
936            vis(&NamedReference::new(item, SmolStr::new_static("text")).into(), N);
937            vis(&NamedReference::new(item, SmolStr::new_static("font-family")).into(), N);
938            vis(&NamedReference::new(item, SmolStr::new_static("font-size")).into(), N);
939            vis(&NamedReference::new(item, SmolStr::new_static("font-weight")).into(), N);
940            vis(&NamedReference::new(item, SmolStr::new_static("letter-spacing")).into(), N);
941            vis(&NamedReference::new(item, SmolStr::new_static("wrap")).into(), N);
942            let wrap_set = item.borrow().is_binding_set("wrap", false)
943                || item
944                    .borrow()
945                    .property_analysis
946                    .borrow()
947                    .get("wrap")
948                    .is_some_and(|a| a.is_set || a.is_set_externally);
949            if wrap_set && orientation == Orientation::Vertical {
950                vis(&NamedReference::new(item, SmolStr::new_static("width")).into(), N);
951            }
952            if base_type.as_str() == "TextInput" {
953                vis(&NamedReference::new(item, SmolStr::new_static("single-line")).into(), N);
954            } else {
955                vis(&NamedReference::new(item, SmolStr::new_static("overflow")).into(), N);
956            }
957        }
958
959        _ => (),
960    }
961}
962
963fn visit_builtin_property(
964    builtin: &crate::langtype::BuiltinElement,
965    prop: &PropertyPath,
966    context: &mut AnalysisContext,
967    reverse_aliases: &ReverseAliases,
968    diag: &mut BuildDiagnostics,
969) {
970    let name = prop.prop.name();
971    if builtin.name == "Window" {
972        for (p, orientation) in
973            [("width", Orientation::Horizontal), ("height", Orientation::Vertical)]
974        {
975            if name == p {
976                // find the actual root component
977                let is_root = |e: &ElementRc| -> bool {
978                    ElementRc::ptr_eq(
979                        e,
980                        &e.borrow().enclosing_component.upgrade().unwrap().root_element,
981                    )
982                };
983                let mut root = prop.prop.element();
984                if !is_root(&root) {
985                    return;
986                };
987                for e in prop.elements.iter().rev() {
988                    if !is_root(&e.0) {
989                        return;
990                    }
991                    root = e.0.clone();
992                }
993                if let Some(p) = root.borrow().layout_info_prop(orientation) {
994                    let path = PropertyPath::from(p.clone());
995                    let old_layout = context.window_layout_property.replace(path.clone());
996                    process_property(&path, ReadType::NativeRead, context, reverse_aliases, diag);
997                    context.window_layout_property = old_layout;
998                };
999            }
1000        }
1001    }
1002}
1003
1004/// Analyze the Window default-font-size property
1005fn check_window_properties(doc: &Document, global_analysis: &mut GlobalAnalysis) {
1006    doc.visit_all_used_components(|component| {
1007        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1008            component,
1009            &(),
1010            &mut |elem, _| {
1011                if elem.borrow().builtin_type().as_ref().is_some_and(|b| b.name == "Window") {
1012                    const DEFAULT_FONT_SIZE: &str = "default-font-size";
1013                    if elem.borrow().is_binding_set(DEFAULT_FONT_SIZE, false)
1014                        || elem
1015                            .borrow()
1016                            .property_analysis
1017                            .borrow()
1018                            .get(DEFAULT_FONT_SIZE)
1019                            .is_some_and(|a| a.is_set)
1020                    {
1021                        let value = elem.borrow().bindings.get(DEFAULT_FONT_SIZE).and_then(|e| {
1022                            match &e.borrow().expression {
1023                                Expression::NumberLiteral(v, crate::expression_tree::Unit::Px) => {
1024                                    Some(*v as f32)
1025                                }
1026                                _ => None,
1027                            }
1028                        });
1029                        let is_const = value.is_some()
1030                            || NamedReference::new(elem, SmolStr::new_static(DEFAULT_FONT_SIZE))
1031                                .is_constant();
1032                        global_analysis.default_font_size = match global_analysis.default_font_size
1033                        {
1034                            DefaultFontSize::Unknown => match value {
1035                                Some(v) => DefaultFontSize::LogicalValue(v),
1036                                None if is_const => DefaultFontSize::Const,
1037                                None => DefaultFontSize::Variable,
1038                            },
1039                            DefaultFontSize::NotSet if is_const => DefaultFontSize::NotSet,
1040                            DefaultFontSize::LogicalValue(val) => match value {
1041                                Some(v) if v == val => DefaultFontSize::LogicalValue(val),
1042                                _ if is_const => DefaultFontSize::Const,
1043                                _ => DefaultFontSize::Variable,
1044                            },
1045                            DefaultFontSize::Const if is_const => DefaultFontSize::Const,
1046                            _ => DefaultFontSize::Variable,
1047                        }
1048                    } else {
1049                        global_analysis.default_font_size = match global_analysis.default_font_size
1050                        {
1051                            DefaultFontSize::Unknown => DefaultFontSize::NotSet,
1052                            DefaultFontSize::NotSet => DefaultFontSize::NotSet,
1053                            DefaultFontSize::LogicalValue(_) => DefaultFontSize::NotSet,
1054                            DefaultFontSize::Const => DefaultFontSize::NotSet,
1055                            DefaultFontSize::Variable => DefaultFontSize::Variable,
1056                        }
1057                    }
1058                }
1059            },
1060        );
1061    });
1062}
1063
1064/// Make sure that the is_set property analysis is set to any property which has a two way binding
1065/// to a property that is, itself, is set
1066///
1067/// Example:
1068/// ```slint
1069/// Xx := TouchArea {
1070///    property <int> bar <=> foo;
1071///    clicked => { bar+=1; }
1072///    property <int> foo; // must ensure that this is not considered as const, because the alias with bar
1073/// }
1074/// ```
1075fn propagate_is_set_on_aliases(doc: &Document, reverse_aliases: &mut ReverseAliases) {
1076    doc.visit_all_used_components(|component| {
1077        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1078            component,
1079            &(),
1080            &mut |e, _| visit_element(e, reverse_aliases),
1081        );
1082    });
1083
1084    fn visit_element(e: &ElementRc, reverse_aliases: &mut ReverseAliases) {
1085        for (name, binding) in &e.borrow().bindings {
1086            if !binding.borrow().two_way_bindings.is_empty() {
1087                check_alias(e, name, &binding.borrow());
1088
1089                let nr = NamedReference::new(e, name.clone());
1090                for a in &binding.borrow().two_way_bindings {
1091                    if let Some(a) = a.property()
1092                        && a != &nr
1093                        && !a.element().borrow().enclosing_component.upgrade().unwrap().is_global()
1094                    {
1095                        reverse_aliases.entry(a.clone()).or_default().push(nr.clone())
1096                    }
1097                }
1098            }
1099        }
1100        for decl in e.borrow().property_declarations.values() {
1101            if let Some(alias) = &decl.is_alias {
1102                mark_alias(alias)
1103            }
1104        }
1105    }
1106
1107    fn check_alias(e: &ElementRc, name: &SmolStr, binding: &BindingExpression) {
1108        // Note: since the analysis hasn't been run, any property access will result in a non constant binding. this is slightly non-optimal
1109        let is_binding_constant =
1110            binding.is_constant(None) && binding.two_way_bindings.iter().all(|n| n.is_constant());
1111        if is_binding_constant && !NamedReference::new(e, name.clone()).is_externally_modified() {
1112            for alias in binding.two_way_bindings.iter().filter_map(|x| x.property()) {
1113                crate::namedreference::mark_property_set_derived_in_base(
1114                    alias.element(),
1115                    alias.name(),
1116                );
1117            }
1118            return;
1119        }
1120
1121        propagate_alias(binding);
1122    }
1123
1124    fn propagate_alias(binding: &BindingExpression) {
1125        for alias in binding.two_way_bindings.iter().filter_map(|x| x.property()) {
1126            mark_alias(alias);
1127        }
1128    }
1129
1130    fn mark_alias(alias: &NamedReference) {
1131        alias.mark_as_set();
1132        if !alias.is_externally_modified()
1133            && let Some(bind) = alias.element().borrow().bindings.get(alias.name())
1134        {
1135            propagate_alias(&bind.borrow())
1136        }
1137    }
1138}
1139
1140/// Make sure that the is_set_externally is true for all bindings.
1141/// And change bindings are used externally
1142fn mark_used_base_properties(doc: &Document) {
1143    doc.visit_all_used_components(|component| {
1144        crate::object_tree::recurse_elem_including_sub_components_no_borrow(
1145            component,
1146            &(),
1147            &mut |element, _| {
1148                if !matches!(element.borrow().base_type, ElementType::Component(_)) {
1149                    return;
1150                }
1151                for (name, binding) in &element.borrow().bindings {
1152                    if binding.borrow().has_binding() {
1153                        crate::namedreference::mark_property_set_derived_in_base(
1154                            element.clone(),
1155                            name,
1156                        );
1157                    }
1158                }
1159                for name in element.borrow().change_callbacks.keys() {
1160                    crate::namedreference::mark_property_read_derived_in_base(
1161                        element.clone(),
1162                        name,
1163                    );
1164                }
1165            },
1166        );
1167    });
1168}