Repository: slint-ui/slint (language: rust)

Issue:
Failure to compile layout: binding loop reported
### Bug Description

After https://github.com/slint-ui/slint/pull/7680 , in particular commit bf716ff8c50ccf3f4b247d17f1ed0d3c4d71fb09, the following provided test-case fails to compile.

It compiles when reverting that change.

Error message:

```bash
error: The binding for the property 'layout-cache' is part of a binding loop
  --> /Users/simon/test.slint:8:5
   |
8  |     HorizontalLayout {
   |     ^
error: The binding for the property 'width' is part of a binding loop
  --> /Users/simon/test.slint:8:5
   |
8  |     HorizontalLayout {
   |     ^
error: The binding for the property 'width' is part of a binding loop
  --> /Users/simon/test.slint:10:13
   |
10 |             Foo { }
   |             ^
error: The binding for the property 'layoutinfo-h' is part of a binding loop
  --> /Users/simon/test.slint:2:5
   |
2  |     VerticalLayout {
   |     ^
error: The binding for the property 'layoutinfo-h' is part of a binding loop
  --> /Users/simon/test.slint:10:13
   |
10 |             Foo { }
   |             ^
error: The binding for the property 'layoutinfo-h' is part of a binding loop
  --> /Users/simon/test.slint:9:9
   |
9  |         Rectangle {
   |         ^
```

### Reproducible Code (if applicable)

```slint
component Foo inherits Rectangle {
    VerticalLayout {
        if root.width >= 10px: HorizontalLayout { }
    }
}

export component AppShell {
    HorizontalLayout {
        Rectangle {
            Foo { }
        }
    }
}
```

### Environment Details

- Slint Version:
- Platform/OS:
- Programming Language:
- Backend/Renderer:


### Product Impact

_No response_


Discussion / hints:
At first glance... I wonder if this is perhaps a valid error and it was just somehow masked before?
Looks like a layout bug in which layout behave differently depending on inlining:

[Example](https://slintpad.com/?snippet=component+Abc+inherits+Rectangle+%7B%0A++++VerticalLayout+%7B%0A++++++++Rectangle+%7B%0A++++++++++++min-width%3A+100px%3B%0A++++++++%7D%0A++++%7D%0A++++%40children%0A%7D%0A%0Acomponent+T1+%7B%0A++++HorizontalLayout+%7B%0A++++++++Rectangle+%7B%0A++++++++++++Abc+%7B+%7D%0A++++++++%7D%0A++++%7D%0A%7D%0A%2F%2F+Manually+inline+Abc%0Acomponent+T2+%7B%0A++++HorizontalLayout+%7B%0A++++++++Rectangle+%7B%0A++++++++++++Rectangle+%7B%0A++++++++++++++++VerticalLayout+%7B%0A++++++++++++++++++++Rectangle+%7B%0A++++++++++++++++++++++++min-width%3A+100px%3B%0A++++++++++++++++++++%7D%0A++++++++++++++++%7D%0A++++++++++++%7D%0A++++++++%7D%0A++++%7D%0A%7D%0A%0A%2F%2F+Force+inlining+because+of+a+children%0Acomponent+T3+%7B%0A++++HorizontalLayout+%7B%0A++++++++Rectangle+%7B%0A++++++++++++Abc+%7B+Rectangle%7B%7D+%7D%0A++++++++%7D%0A++++%7D%0A%7D%0A%0Aexport+component+X+inherits+Rectangle+%7B%0A++++t1%3A%3D+T1+%7B%7D%0A++++t2%3A%3D+T2+%7B%7D%0A++++t3%3A%3D+T3+%7B%7D%0A++++Text+%7B%0A++++++++text%3A+%22t1%3A%22+%2B+t1.min-width%2F1px+%2B+%22++t2%3A%22+%2B+t2.min-width%2F1px+%2B+%22++t3%3A%22+%2B+t3.min-width%2F1px%3B%0A++++%7D%0A%7D)

In that example, i'd have expected t1 and t2 and t3 to have the same minimal size.
But in 1.9.2 they don't have the same size because we somehow don't propagate the constraints from components
I tried to "fix" the problem by removing this lines
https://github.com/slint-ui/slint/blob/34b115bc9f118d6791c29ba7b1adbb886eb34c8d/internal/compiler/passes/default_geometry.rs#L213-L215

(indeed, when we see something like `abc := Abc {}`, then the element abc has no children info directly, but it still has some "implicit" constraint that we should take care of for the layout info from Abc's component children.)

But then we panic later in other testcase in 
```
panicked at internal/compiler/passes/inlining.rs:253:13:
not yet implemented: Merge layout infos
```

Which would need to be fixed but i don't have the time to do that now.

Another similar bug in https://github.com/slint-ui/slint/actions/runs/13460934634/job/37615873276
https://github.com/somantics/retrodungeon/blob/9f8b7b5aa0d5d68b331c6a710b9c6dff42011447/ui/old.slint#L122

```
  error: The property 'y' cannot be set for elements placed in this layout, because the layout is already setting it
     --> /home/runner/the_repo/ui/old.slint:122:6
      |
  122 |   y: 0;
      |      ^
```

This is again caused because of inlining and the `y` property on the root cause that. which is yet another bug.

I think the best is to revert commit https://github.com/slint-ui/slint/commit/bf716ff8c50ccf3f4b247d17f1ed0d3c4d71fb09 for this release and try to fix these bug before we do the inlining.


Edit: another bug caused by that commit in https://github.com/slint-ui/slint/actions/runs/13460934634/job/37615849402 when compiling https://github.com/DASPRiD/vrc-osc-manager
```
  thread 'main' panicked at /home/olivier/slint/internal/compiler/llr/lower_expression.rs:43:34:
  called `Option::unwrap()` on a `None` value
```

Fix patch:
diff --git a/internal/compiler/passes/inlining.rs b/internal/compiler/passes/inlining.rs
index 04ffed4c9d6..37978c368c5 100644
--- a/internal/compiler/passes/inlining.rs
+++ b/internal/compiler/passes/inlining.rs
@@ -23,7 +23,6 @@ pub fn inline(doc: &Document, inline_selection: InlineSelection, diag: &mut Buil
     fn inline_components_recursively(
         component: &Rc<Component>,
         roots: &HashSet<ByAddress<Rc<Component>>>,
-        used_once: &HashSet<ByAddress<Rc<Component>>>,
         inline_selection: InlineSelection,
         diag: &mut BuildDiagnostics,
     ) {
@@ -31,7 +30,7 @@ pub fn inline(doc: &Document, inline_selection: InlineSelection, diag: &mut Buil
             let base = elem.borrow().base_type.clone();
             if let ElementType::Component(c) = base {
                 // First, make sure that the component itself is properly inlined
-                inline_components_recursively(&c, roots, used_once, inline_selection, diag);
+                inline_components_recursively(&c, roots, inline_selection, diag);
 
                 if c.parent_element.upgrade().is_some() {
                     // We should not inline a repeated element
@@ -49,7 +48,6 @@ pub fn inline(doc: &Document, inline_selection: InlineSelection, diag: &mut Buil
                             || component.parent_element.upgrade().is_none() && Rc::ptr_eq(elem, &component.root_element)
                             // We always inline other roots as a component can't be both a sub component and a root
                             || roots.contains(&ByAddress(c.clone()))
-                            || used_once.contains(&ByAddress(c.clone()))
                     }
                 } {
                     inline_element(elem, &c, component, diag);
@@ -57,19 +55,17 @@ pub fn inline(doc: &Document, inline_selection: InlineSelection, diag: &mut Buil
             }
         });
         component.popup_windows.borrow().iter().for_each(|p| {
-            inline_components_recursively(&p.component, roots, used_once, inline_selection, diag)
+            inline_components_recursively(&p.component, roots, inline_selection, diag)
         })
     }
     let mut roots = HashSet::new();
-    let mut used_once = HashSet::new();
     if inline_selection == InlineSelection::InlineOnlyRequiredComponents {
         for component in doc.exported_roots().chain(doc.popup_menu_impl.iter().cloned()) {
             roots.insert(ByAddress(component.clone()));
         }
-        used_once = collect_subcomponents_used_once(doc);
     }
     for component in doc.exported_roots().chain(doc.popup_menu_impl.iter().cloned()) {
-        inline_components_recursively(&component, &roots, &used_once, inline_selection, diag);
+        inline_components_recursively(&component, &roots, inline_selection, diag);
         let mut init_code = component.init_code.borrow_mut();
         let inlined_init_code = core::mem::take(&mut init_code.inlined_init_code);
         init_code.constructor_code.splice(0..0, inlined_init_code.into_values());
@@ -655,35 +651,3 @@ fn element_require_inlining(elem: &ElementRc) -> bool {
 
     false
 }
-
-fn collect_subcomponents_used_once(doc: &Document) -> HashSet<ByAddress<Rc<Component>>> {
-    fn recursive(component: &Rc<Component>, hash: &mut HashMap<ByAddress<Rc<Component>>, bool>) {
-        match hash.entry(ByAddress(component.clone())) {
-            std::collections::hash_map::Entry::Occupied(mut o) => {
-                if !*o.get() {
-                    // We have already set this element (and its children to "set multiple times"
-                    return;
-                }
-                // Element used multiple times, visit all children to set them to multiple times too.
-                *o.get_mut() = false;
-            }
-            std::collections::hash_map::Entry::Vacant(v) => {
-                v.insert(true);
-            }
-        }
-
-        recurse_elem(&component.root_element, &(), &mut |elem: &ElementRc, &()| {
-            let ElementType::Component(base_comp) = &elem.borrow().base_type else { return };
-            recursive(base_comp, hash);
-        });
-        for popup in component.popup_windows.borrow().iter() {
-            recursive(&popup.component, hash);
-        }
-    }
-    // Stores true for elements that are used once, false for elements that are used multiple times;
-    let mut result = HashMap::new();
-    for component in doc.exported_roots().chain(doc.popup_menu_impl.iter().cloned()) {
-        recursive(&component, &mut result);
-    }
-    result.into_iter().filter(|(_, v)| *v).map(|(k, _)| k).collect()
-}


Test patch:
diff --git a/internal/compiler/tests/syntax/analysis/binding_loop_layout.slint b/internal/compiler/tests/syntax/analysis/binding_loop_layout.slint
index a66b593a9c3..05e71982a19 100644
--- a/internal/compiler/tests/syntax/analysis/binding_loop_layout.slint
+++ b/internal/compiler/tests/syntax/analysis/binding_loop_layout.slint
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
 
 TC := Rectangle {
+//    ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
     outer := VerticalLayout {
 //           ^error{The binding for the property 'width' is part of a binding loop}
 //           ^^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
@@ -45,7 +46,6 @@ export Test := Rectangle {
 
     tc := TC {
 //        ^error{The binding for the property 'preferred-width' is part of a binding loop}
-//        ^^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
         width: preferred-width;
 //             ^error{The binding for the property 'width' is part of a binding loop}
     }
diff --git a/internal/compiler/tests/syntax/analysis/binding_loop_layout3.slint b/internal/compiler/tests/syntax/analysis/binding_loop_layout3.slint
index 36463db1f88..2c77dfd5be8 100644
--- a/internal/compiler/tests/syntax/analysis/binding_loop_layout3.slint
+++ b/internal/compiler/tests/syntax/analysis/binding_loop_layout3.slint
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
 
 Compo := Rectangle {
+//       ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
 
     property <string> the_text;
 
@@ -24,7 +25,6 @@ Compo := Rectangle {
 export Foo := Rectangle {
     Compo {
 //  ^error{The binding for the property 'preferred-width' is part of a binding loop}
-//  ^^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
         the_text: self.preferred-width / 1px;
 //                ^error{The binding for the property 'the-text' is part of a binding loop}
     }
diff --git a/internal/compiler/tests/syntax/analysis/binding_loop_layout4.slint b/internal/compiler/tests/syntax/analysis/binding_loop_layout4.slint
index 15c39a4ad6f..7ee6aa38bdb 100644
--- a/internal/compiler/tests/syntax/analysis/binding_loop_layout4.slint
+++ b/internal/compiler/tests/syntax/analysis/binding_loop_layout4.slint
@@ -24,6 +24,7 @@ component Foo {
 }
 
 component Bar {
+//            ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
     HorizontalLayout {
 //  ^error{The binding for the property 'layout-cache' is part of a binding loop}
 //  ^^error{The binding for the property 'width' is part of a binding loop}
@@ -39,6 +40,4 @@ export component Apps inherits Window {
     Bar {}
 //  ^error{The binding for the property 'preferred-width' is part of a binding loop}
 //  ^^error{The binding for the property 'width' is part of a binding loop}
-//  ^^^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
-//  ^^^^error{The binding for the property 'min-width' is part of a binding loop}
 }
diff --git a/internal/compiler/tests/syntax/analysis/binding_loop_layout_if.slint b/internal/compiler/tests/syntax/analysis/binding_loop_layout_if.slint
index f8113b92128..74403aba907 100644
--- a/internal/compiler/tests/syntax/analysis/binding_loop_layout_if.slint
+++ b/internal/compiler/tests/syntax/analysis/binding_loop_layout_if.slint
@@ -15,6 +15,7 @@ component Wrapper {
 }
 
 component WrapperInherited inherits Rectangle {
+//                                  ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
     VerticalLayout {
 //  ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
         if root.width > 200px: Rectangle { }
@@ -29,9 +30,8 @@ export component Test inherits Window {
 //      ^^error{The binding for the property 'width' is part of a binding loop}
 //      ^^^error{The binding for the property 'layout-cache' is part of a binding loop}
             WrapperInherited { }
-//          ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
             Wrapper { }
 //          ^error{The binding for the property 'layoutinfo-h' is part of a binding loop}
         }
     }
-}
+}
\ No newline at end of file
diff --git a/internal/compiler/tests/syntax/basic/dialog.slint b/internal/compiler/tests/syntax/basic/dialog.slint
index dfe346ab676..19ffd4db9ef 100644
--- a/internal/compiler/tests/syntax/basic/dialog.slint
+++ b/internal/compiler/tests/syntax/basic/dialog.slint
@@ -11,6 +11,7 @@ MyDiag1 := Dialog {
 }
 
 MyDiag2 := Dialog {
+//         ^error{A Dialog must have a single child element that is not StandardButton}
     StandardButton { kind: reset; }
     StandardButton {
         kind: cancel;
@@ -54,7 +55,6 @@ MyDialog4 := Dialog {
 export Test := Rectangle {
     MyDiag1 {}
     MyDiag2 {}
-//  ^error{A Dialog must have a single child element that is not StandardButton}
     MyDiag3 {}
     MyDialog4 {}
 }