slint/internal/compiler/passes/remove_aliases.rs
Olivier Goffart f100ee48ff Fix alias to externaly modified property not being marked as such
In the case of bug #3318, the implicit alias to the window's height or
width was removed but the propery was not marked as externaly modified,
causing later pass to optimize and inline things that shouldn't

Fixes #3318
2023-08-22 16:20:13 +02:00

234 lines
9 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
//! This pass removes the property used in a two ways bindings
use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::{BindingExpression, Expression, NamedReference};
use crate::object_tree::*;
use std::cell::RefCell;
use std::collections::{btree_map::Entry, HashMap, HashSet};
use std::rc::Rc;
// The property in the key is to be removed, and replaced by the property in the value
type Mapping = HashMap<NamedReference, NamedReference>;
#[derive(Default, Debug)]
struct PropertySets {
map: HashMap<NamedReference, Rc<RefCell<HashSet<NamedReference>>>>,
all_sets: Vec<Rc<RefCell<HashSet<NamedReference>>>>,
}
impl PropertySets {
fn add_link(&mut self, p1: NamedReference, p2: NamedReference) {
if !std::rc::Weak::ptr_eq(
&p1.element().borrow().enclosing_component,
&p2.element().borrow().enclosing_component,
) && !p1.element().borrow().enclosing_component.upgrade().unwrap().is_global()
&& !p2.element().borrow().enclosing_component.upgrade().unwrap().is_global()
{
// We can only merge aliases if they are in the same Component. (unless one of them is global)
// TODO: actually we could still merge two alias in a component pointing to the same
// property in a parent component
return;
}
if let Some(s1) = self.map.get(&p1).cloned() {
if let Some(s2) = self.map.get(&p2).cloned() {
if Rc::ptr_eq(&s1, &s2) {
return;
}
for x in s1.borrow().iter() {
self.map.insert(x.clone(), s2.clone());
s2.borrow_mut().insert(x.clone());
}
*s1.borrow_mut() = HashSet::new();
} else {
s1.borrow_mut().insert(p2.clone());
self.map.insert(p2, s1);
}
} else if let Some(s2) = self.map.get(&p2).cloned() {
s2.borrow_mut().insert(p1.clone());
self.map.insert(p1, s2);
} else {
let mut set = HashSet::new();
set.insert(p1.clone());
set.insert(p2.clone());
let set = Rc::new(RefCell::new(set));
self.map.insert(p1, set.clone());
self.map.insert(p2, set.clone());
self.all_sets.push(set)
}
}
}
pub fn remove_aliases(doc: &Document, diag: &mut BuildDiagnostics) {
// collect all sets that are linked together
let mut property_sets = PropertySets::default();
let mut process_element = |e: &ElementRc| {
'bindings: for (name, binding) in &e.borrow().bindings {
for nr in &binding.borrow().two_way_bindings {
let other_e = nr.element();
if name == nr.name() && Rc::ptr_eq(e, &other_e) {
diag.push_error("Property cannot alias to itself".into(), &*binding.borrow());
continue 'bindings;
}
property_sets.add_link(NamedReference::new(e, name), nr.clone());
}
}
};
for component in (doc.root_component.used_types.borrow().sub_components.iter())
.chain(doc.root_component.used_types.borrow().globals.iter())
.chain(std::iter::once(&doc.root_component))
{
recurse_elem_including_sub_components(component, &(), &mut |e, &()| process_element(e));
}
// The key will be removed and replaced by the named reference
let mut aliases_to_remove = Mapping::new();
// For each set, find a "master" property. Only reference to this master property will be kept,
// and only the master property will keep its binding
for set in property_sets.all_sets {
let set = set.borrow();
let mut set_iter = set.iter();
if let Some(mut best) = set_iter.next().cloned() {
for candidate in set_iter {
best = best_property(best.clone(), candidate.clone());
}
for x in set.iter() {
if *x != best {
aliases_to_remove.insert(x.clone(), best.clone());
}
}
}
}
for component in (doc.root_component.used_types.borrow().sub_components.iter())
.chain(doc.root_component.used_types.borrow().globals.iter())
.chain(std::iter::once(&doc.root_component))
{
// Do the replacements
visit_all_named_references(component, &mut |nr: &mut NamedReference| {
if let Some(new) = aliases_to_remove.get(nr) {
*nr = new.clone();
}
});
}
// Remove the properties
for (remove, to) in aliases_to_remove {
let elem = remove.element();
let to_elem = to.element();
// adjust the bindings
let old_binding = elem.borrow_mut().bindings.remove(remove.name());
let mut old_binding = old_binding.map(RefCell::into_inner).unwrap_or_else(|| {
// ensure that we set an expression, because the right hand side of a binding always wins,
// and if that was not set, we must still kee the default then
let mut b = BindingExpression::from(Expression::default_value_for_type(&to.ty()));
b.priority = to_elem
.borrow_mut()
.bindings
.get(to.name())
.map_or(i32::MAX, |x| x.borrow().priority.saturating_add(1));
b
});
remove_from_binding_expression(&mut old_binding, &to);
let same_component = std::rc::Weak::ptr_eq(
&elem.borrow().enclosing_component,
&to_elem.borrow().enclosing_component,
);
match to_elem.borrow_mut().bindings.entry(to.name().to_owned()) {
Entry::Occupied(mut e) => {
let b = e.get_mut().get_mut();
remove_from_binding_expression(b, &to);
if !same_component || b.priority < old_binding.priority || !b.has_binding() {
b.merge_with(&old_binding);
} else {
old_binding.merge_with(b);
*b = old_binding;
}
}
Entry::Vacant(e) => {
if same_component && old_binding.has_binding() {
e.insert(old_binding.into());
}
}
};
// Remove the declaration
{
let mut elem = elem.borrow_mut();
let used_externally = elem
.property_analysis
.borrow()
.get(remove.name())
.map_or(false, |v| v.is_read_externally || v.is_set_externally);
if let Some(d) = elem.property_declarations.get_mut(remove.name()) {
if d.expose_in_public_api || used_externally {
d.is_alias = Some(to.clone());
drop(elem);
// one must mark the aliased property as settable from outside
to.mark_as_set();
} else {
elem.property_declarations.remove(remove.name());
let analysis = elem.property_analysis.borrow().get(remove.name()).cloned();
if let Some(analysis) = analysis {
drop(elem);
to.element()
.borrow()
.property_analysis
.borrow_mut()
.entry(to.name().to_owned())
.or_default()
.merge(&analysis);
};
}
} else {
// This is not a declaration, we must re-create the binding
elem.bindings.insert(
remove.name().to_owned(),
BindingExpression::new_two_way(to.clone()).into(),
);
drop(elem);
if remove.is_externally_modified() {
to.mark_as_set();
}
}
}
}
}
fn is_declaration(x: &NamedReference) -> bool {
x.element().borrow().property_declarations.contains_key(x.name())
}
/// Out of two named reference, return the one which is the best to keep.
fn best_property(p1: NamedReference, p2: NamedReference) -> NamedReference {
// Try to find which is the more canonical property
macro_rules! canonical_order {
($x: expr) => {{
(
!$x.element().borrow().enclosing_component.upgrade().unwrap().is_global(),
is_declaration(&$x),
$x.element().borrow().id.clone(),
$x.name(),
)
}};
}
if canonical_order!(p1) < canonical_order!(p2) {
p1
} else {
p2
}
}
/// Remove the `to` from the two_way_bindings
fn remove_from_binding_expression(expression: &mut BindingExpression, to: &NamedReference) {
expression.two_way_bindings.retain(|x| x != to);
}