// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial //! Compute binding analysis and attempt to find binding loops use std::collections::HashMap; use std::collections::HashSet; use std::rc::Rc; use by_address::ByAddress; use crate::diagnostics::BuildDiagnostics; use crate::diagnostics::Spanned; use crate::expression_tree::BindingExpression; use crate::expression_tree::BuiltinFunction; use crate::expression_tree::Expression; use crate::langtype::ElementType; use crate::layout::LayoutItem; use crate::layout::Orientation; use crate::namedreference::NamedReference; use crate::object_tree::find_parent_element; use crate::object_tree::Document; use crate::object_tree::PropertyAnimation; use crate::object_tree::{Component, ElementRc}; use derive_more as dm; /// Maps the alias in the other direction than what the BindingExpression::two_way_binding does. /// So if binding for property A has B in its BindingExpression::two_way_binding, then /// ReverseAliases maps B to A. type ReverseAliases = HashMap>; pub fn binding_analysis(doc: &Document, diag: &mut BuildDiagnostics) { let component = &doc.root_component; let mut reverse_aliases = Default::default(); mark_used_base_properties(component); propagate_is_set_on_aliases(component, &mut reverse_aliases); perform_binding_analysis(component, &reverse_aliases, diag); } /// A reference to a property which might be deep in a component path. /// eg: `foo.bar.baz.background`: `baz.background` is the `prop` and `foo` and `bar` are in elements #[derive(Hash, PartialEq, Eq, Clone)] struct PropertyPath { elements: Vec>, prop: NamedReference, } impl std::fmt::Debug for PropertyPath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for e in &self.elements { write!(f, "{}.", e.borrow().id)?; } self.prop.fmt(f) } } impl PropertyPath { /// Given a namedReference accessed by something on the same leaf component /// as self, return a new PropertyPath that represent the property pointer /// to by nr in the higher possible element fn relative(&self, second: &PropertyPath) -> Self { let mut element = second.elements.first().map_or_else(|| second.prop.element(), |f| f.0.clone()); if element.borrow().enclosing_component.upgrade().unwrap().is_global() { return second.clone(); } let mut elements = self.elements.clone(); loop { let enclosing = element.borrow().enclosing_component.upgrade().unwrap(); if enclosing.parent_element.upgrade().is_some() || !Rc::ptr_eq(&element, &enclosing.root_element) { break; } if let Some(last) = elements.pop() { #[cfg(debug_assertions)] fn check_that_element_is_in_the_component( e: &ElementRc, c: &Rc, ) -> bool { let enclosing = e.borrow().enclosing_component.upgrade().unwrap(); Rc::ptr_eq(c, &enclosing) || enclosing .parent_element .upgrade() .map_or(false, |e| check_that_element_is_in_the_component(&e, c)) } #[cfg(debug_assertions)] debug_assert!( check_that_element_is_in_the_component( &element, last.borrow().base_type.as_component() ), "The element is not in the component pointed at by the path ({:?} / {:?})", self, second ); element = last.0; } else { break; } } if second.elements.is_empty() { debug_assert!(elements.last().map_or(true, |x| *x != ByAddress(second.prop.element()))); Self { elements, prop: NamedReference::new(&element, second.prop.name()) } } else { elements.push(ByAddress(element)); elements.extend(second.elements.iter().skip(1).cloned()); Self { elements, prop: second.prop.clone() } } } } impl From for PropertyPath { fn from(prop: NamedReference) -> Self { Self { elements: vec![], prop } } } #[derive(Default)] struct AnalysisContext { visited: HashSet, currently_analyzing: linked_hash_set::LinkedHashSet, } fn perform_binding_analysis( component: &Rc, reverse_aliases: &ReverseAliases, diag: &mut BuildDiagnostics, ) { for c in &component.used_types.borrow().sub_components { perform_binding_analysis(c, reverse_aliases, diag); } let mut context = AnalysisContext::default(); crate::object_tree::recurse_elem_including_sub_components_no_borrow( component, &(), &mut |e, _| analyze_element(e, &mut context, reverse_aliases, diag), ); } fn analyze_element( elem: &ElementRc, context: &mut AnalysisContext, reverse_aliases: &ReverseAliases, diag: &mut BuildDiagnostics, ) { for (name, binding) in &elem.borrow().bindings { if binding.borrow().analysis.is_some() { continue; } analyze_binding( &PropertyPath::from(NamedReference::new(elem, name)), context, reverse_aliases, diag, ); } for nr in elem.borrow().accessibility_props.0.values() { process_property(&PropertyPath::from(nr.clone()), context, reverse_aliases, diag); } if let Some(g) = elem.borrow().geometry_props.as_ref() { process_property(&g.x.clone().into(), context, reverse_aliases, diag); process_property(&g.y.clone().into(), context, reverse_aliases, diag); process_property(&g.width.clone().into(), context, reverse_aliases, diag); process_property(&g.height.clone().into(), context, reverse_aliases, diag); } if let Some(component) = elem.borrow().enclosing_component.upgrade() { if Rc::ptr_eq(&component.root_element, elem) { for e in component.init_code.borrow().iter() { recurse_expression(e, &mut |prop| { process_property(prop, context, reverse_aliases, diag); }); } } } if let Some(repeated) = &elem.borrow().repeated { recurse_expression(&repeated.model, &mut |prop| { process_property(prop, context, reverse_aliases, diag); }); if let Some(lv) = &repeated.is_listview { process_property(&lv.viewport_y.clone().into(), context, reverse_aliases, diag); process_property(&lv.viewport_height.clone().into(), context, reverse_aliases, diag); process_property(&lv.viewport_width.clone().into(), context, reverse_aliases, diag); process_property(&lv.listview_height.clone().into(), context, reverse_aliases, diag); process_property(&lv.listview_width.clone().into(), context, reverse_aliases, diag); } } if let Some((h, v)) = &elem.borrow().layout_info_prop { process_property(&h.clone().into(), context, reverse_aliases, diag); process_property(&v.clone().into(), context, reverse_aliases, diag); } } #[derive(Copy, Clone, dm::BitAnd, dm::BitOr, dm::BitAndAssign, dm::BitOrAssign)] struct DependsOnExternal(bool); fn analyze_binding( current: &PropertyPath, context: &mut AnalysisContext, reverse_aliases: &ReverseAliases, diag: &mut BuildDiagnostics, ) -> DependsOnExternal { let mut depends_on_external = DependsOnExternal(false); let element = current.prop.element(); let name = current.prop.name(); if context.currently_analyzing.back().map_or(false, |r| r == current) && !element.borrow().bindings[name].borrow().two_way_bindings.is_empty() { let span = element.borrow().bindings[name] .borrow() .span .clone() .unwrap_or_else(|| element.borrow().to_source_location()); diag.push_error(format!("Property '{name}' cannot refer to itself"), &span); return depends_on_external; } if context.currently_analyzing.contains(current) { for it in context.currently_analyzing.iter().rev() { let p = &it.prop; let elem = p.element(); let elem = elem.borrow(); let binding = elem.bindings[p.name()].borrow(); if binding.analysis.as_ref().unwrap().is_in_binding_loop.replace(true) { break; } diag.push_error( format!("The binding for the property '{}' is part of a binding loop", p.name()), &binding.span.clone().unwrap_or_else(|| elem.to_source_location()), ); if it == current { break; } } return depends_on_external; } let binding = &element.borrow().bindings[name]; if binding.borrow().analysis.as_ref().map_or(false, |a| a.no_external_dependencies) { return depends_on_external; } else if !context.visited.insert(current.clone()) { return DependsOnExternal(true); } if let Ok(mut b) = binding.try_borrow_mut() { b.analysis = Some(Default::default()); }; context.currently_analyzing.insert(current.clone()); let b = binding.borrow(); for nr in &b.two_way_bindings { if nr != ¤t.prop { depends_on_external |= process_property( ¤t.relative(&nr.clone().into()), context, reverse_aliases, diag, ); } } let mut process_prop = |prop: &PropertyPath| { depends_on_external |= process_property(¤t.relative(prop), context, reverse_aliases, diag); for x in reverse_aliases.get(&prop.prop).unwrap_or(&Default::default()) { if x != ¤t.prop && x != &prop.prop { depends_on_external |= process_property( ¤t.relative(&x.clone().into()), context, reverse_aliases, diag, ); } } }; recurse_expression(&b.expression, &mut process_prop); let mut is_const = b.expression.is_constant() && b.two_way_bindings.iter().all(|n| n.is_constant()); if is_const && matches!(b.expression, Expression::Invalid) { // check the base if let Some(base) = element.borrow().sub_component() { is_const = NamedReference::new(&base.root_element, name).is_constant(); } } drop(b); if let Ok(mut b) = binding.try_borrow_mut() { // We have a loop (through different component so we're still borrowed) b.analysis.as_mut().unwrap().is_const = is_const; } match &binding.borrow().animation { Some(PropertyAnimation::Static(e)) => analyze_element(e, context, reverse_aliases, diag), Some(PropertyAnimation::Transition { animations, state_ref }) => { recurse_expression(state_ref, &mut process_prop); for a in animations { analyze_element(&a.animation, context, reverse_aliases, diag); } } None => (), } let o = context.currently_analyzing.pop_back(); assert_eq!(&o.unwrap(), current); depends_on_external } /// Process the property `prop` /// /// This will visit all the bindings from that property fn process_property( prop: &PropertyPath, context: &mut AnalysisContext, reverse_aliases: &ReverseAliases, diag: &mut BuildDiagnostics, ) -> DependsOnExternal { let depends_on_external = match prop .prop .element() .borrow() .property_analysis .borrow_mut() .entry(prop.prop.name().into()) .or_default() { a => { a.is_read = true; DependsOnExternal(prop.elements.is_empty() && a.is_set_externally) } }; let mut prop = prop.clone(); loop { let element = prop.prop.element(); if element.borrow().bindings.contains_key(prop.prop.name()) { analyze_binding(&prop, context, reverse_aliases, diag); } let next = if let ElementType::Component(base) = &element.borrow().base_type { if element.borrow().property_declarations.contains_key(prop.prop.name()) { break; } base.root_element.clone() } else { break; }; next.borrow() .property_analysis .borrow_mut() .entry(prop.prop.name().into()) .or_default() .is_read_externally = true; prop.elements.push(element.into()); prop.prop = NamedReference::new(&next, prop.prop.name()); } depends_on_external } // Same as in crate::visit_all_named_references_in_element, but not mut fn recurse_expression(expr: &Expression, vis: &mut impl FnMut(&PropertyPath)) { expr.visit(|sub| recurse_expression(sub, vis)); match expr { Expression::PropertyReference(r) | Expression::CallbackReference(r, _) | Expression::FunctionReference(r, _) => vis(&r.clone().into()), Expression::LayoutCacheAccess { layout_cache_prop, .. } => { vis(&layout_cache_prop.clone().into()) } Expression::SolveLayout(l, o) | Expression::ComputeLayoutInfo(l, o) => { // we should only visit the layout geometry for the orientation if matches!(expr, Expression::SolveLayout(..)) { if let Some(nr) = l.rect().size_reference(*o) { vis(&nr.clone().into()); } } match l { crate::layout::Layout::GridLayout(l) => { visit_layout_items_dependencies(l.elems.iter().map(|it| &it.item), *o, vis) } crate::layout::Layout::BoxLayout(l) => { visit_layout_items_dependencies(l.elems.iter(), *o, vis) } } let mut g = l.geometry().clone(); g.rect = Default::default(); // already visited; g.visit_named_references(&mut |nr| vis(&nr.clone().into())) } Expression::FunctionCall { function, arguments, .. } => match &**function { Expression::BuiltinFunctionReference( BuiltinFunction::ImplicitLayoutInfo(orientation), _, ) => { if let [Expression::ElementReference(item)] = arguments.as_slice() { visit_implicit_layout_info_dependencies( *orientation, &item.upgrade().unwrap(), vis, ); } } Expression::BuiltinFunctionReference(BuiltinFunction::ItemAbsolutePosition, _) => { if let Some(Expression::ElementReference(item)) = arguments.first() { let mut item = item.upgrade().unwrap(); while let Some(parent) = find_parent_element(&item) { item = parent; vis(&NamedReference::new(&item, "x").into()); vis(&NamedReference::new(&item, "y").into()); } } } _ => {} }, _ => {} } } fn visit_layout_items_dependencies<'a>( items: impl Iterator, orientation: Orientation, vis: &mut impl FnMut(&PropertyPath), ) { for it in items { let mut element = it.element.clone(); if element.borrow().repeated.is_some() { element = it.element.borrow().base_type.as_component().root_element.clone(); } if let Some(nr) = element.borrow().layout_info_prop(orientation) { vis(&nr.clone().into()); } else { if let ElementType::Component(base) = &element.borrow().base_type { if let Some(nr) = base.root_element.borrow().layout_info_prop(orientation) { vis(&PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone(), }); } } visit_implicit_layout_info_dependencies(orientation, &element, vis); } for (nr, _) in it.constraints.for_each_restrictions(orientation) { vis(&nr.clone().into()) } } } /// The builtin function can call native code, and we need to visit the properties that are accessed by it fn visit_implicit_layout_info_dependencies( orientation: crate::layout::Orientation, item: &ElementRc, vis: &mut impl FnMut(&PropertyPath), ) { let base_type = item.borrow().base_type.to_string(); match base_type.as_str() { "Image" => { vis(&NamedReference::new(item, "source").into()); if orientation == Orientation::Vertical { vis(&NamedReference::new(item, "width").into()); } } "Text" | "TextInput" => { vis(&NamedReference::new(item, "text").into()); vis(&NamedReference::new(item, "font-family").into()); vis(&NamedReference::new(item, "font-size").into()); vis(&NamedReference::new(item, "font-weight").into()); vis(&NamedReference::new(item, "letter-spacing").into()); vis(&NamedReference::new(item, "wrap").into()); let wrap_set = item.borrow().is_binding_set("wrap", false) || item .borrow() .property_analysis .borrow() .get("wrap") .map_or(false, |a| a.is_set || a.is_set_externally); if wrap_set && orientation == Orientation::Vertical { vis(&NamedReference::new(item, "width").into()); } if base_type.as_str() == "TextInput" { vis(&NamedReference::new(item, "single-line").into()); } else { vis(&NamedReference::new(item, "overflow").into()); } } _ => (), } } /// Make sure that the is_set property analysis is set to any property which has a two way binding /// to a property that is, itself, is set /// /// Example: /// ```slint /// Xx := TouchArea { /// property bar <=> foo; /// clicked => { bar+=1; } /// property foo; // must ensure that this is not considered as const, because the alias with bar /// } /// ``` fn propagate_is_set_on_aliases(component: &Rc, reverse_aliases: &mut ReverseAliases) { crate::object_tree::recurse_elem_including_sub_components_no_borrow( component, &(), &mut |e, _| { for (name, binding) in &e.borrow().bindings { if !binding.borrow().two_way_bindings.is_empty() { check_alias(e, name, &binding.borrow()); let nr = NamedReference::new(e, name); for a in &binding.borrow().two_way_bindings { if a != &nr && !a .element() .borrow() .enclosing_component .upgrade() .unwrap() .is_global() { reverse_aliases.entry(a.clone()).or_default().push(nr.clone()) } } } } for decl in e.borrow().property_declarations.values() { if let Some(alias) = &decl.is_alias { mark_alias(alias) } } }, ); fn check_alias(e: &ElementRc, name: &str, binding: &BindingExpression) { // Note: since the analysis hasn't been run, any property access will result in a non constant binding. this is slightly non-optimal let is_binding_constant = binding.is_constant() && binding.two_way_bindings.iter().all(|n| n.is_constant()); if is_binding_constant && !NamedReference::new(e, name).is_externally_modified() { for alias in &binding.two_way_bindings { crate::namedreference::mark_property_set_derived_in_base( alias.element(), alias.name(), ); } return; } propagate_alias(binding); } fn propagate_alias(binding: &BindingExpression) { for alias in &binding.two_way_bindings { mark_alias(alias); } } fn mark_alias(alias: &NamedReference) { alias.mark_as_set(); if !alias.is_externally_modified() { if let Some(bind) = alias.element().borrow().bindings.get(alias.name()) { propagate_alias(&bind.borrow()) } } } for c in &component.used_types.borrow().sub_components { propagate_is_set_on_aliases(c, reverse_aliases); } } /// Make sure that the is_set_externally is true for all bindings fn mark_used_base_properties(component: &Rc) { crate::object_tree::recurse_elem_including_sub_components_no_borrow( component, &(), &mut |element, _| { if !matches!(element.borrow().base_type, ElementType::Component(_)) { return; } for (name, binding) in &element.borrow().bindings { if binding.borrow().has_binding() { crate::namedreference::mark_property_set_derived_in_base(element.clone(), name); } } }, ); for c in &component.used_types.borrow().sub_components { mark_used_base_properties(c); } }