Repository: slint-ui/slint
Pull request #8051: compiler: Add a DebugHook expression
Author: login: hunger
Created: 2025-04-03T17:14:13Z | Merged: 2025-04-09T11:52:30Z

You can not create this expression manually, but there is a pass in the compiler that adds it to all set
properties in a compilation run.

All it does is basically associate an id with an expression, so that we can then in a later step have the interpreter do something with that information. Apart from that, it tries to be as transparent as possible.

The LLR lowering removes that expression again, just so we can be sure it does not end up in the generated live code.


Diff:
diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs
index 0eac7b359f2..7bd6d2a1265 100644
--- a/internal/compiler/expression_tree.rs
+++ b/internal/compiler/expression_tree.rs
@@ -716,6 +716,11 @@ pub enum Expression {
         rhs: Box<Expression>,
     },
 
+    DebugHook {
+        expression: Box<Expression>,
+        id: SmolStr,
+    },
+
     EmptyComponentFactory,
 }
 
@@ -837,6 +842,7 @@ impl Expression {
             Expression::SolveLayout(..) => Type::LayoutCache,
             Expression::MinMax { ty, .. } => ty.clone(),
             Expression::EmptyComponentFactory => Type::ComponentFactory,
+            Expression::DebugHook { expression, .. } => expression.ty(),
         }
     }
 
@@ -931,6 +937,7 @@ impl Expression {
                 visitor(rhs);
             }
             Expression::EmptyComponentFactory => {}
+            Expression::DebugHook { expression, .. } => visitor(expression),
         }
     }
 
@@ -1027,6 +1034,7 @@ impl Expression {
                 visitor(rhs);
             }
             Expression::EmptyComponentFactory => {}
+            Expression::DebugHook { expression, .. } => visitor(expression),
         }
     }
 
@@ -1108,6 +1116,7 @@ impl Expression {
             Expression::SolveLayout(..) => false,
             Expression::MinMax { lhs, rhs, .. } => lhs.is_constant() && rhs.is_constant(),
             Expression::EmptyComponentFactory => true,
+            Expression::DebugHook { .. } => false,
         }
     }
 
@@ -1409,6 +1418,14 @@ impl Expression {
             }
         }
     }
+
+    /// Unwrap DebugHook expressions to their contained sub-expression
+    pub fn ignore_debug_hooks(&self) -> &Expression {
+        match self {
+            Expression::DebugHook { expression, .. } => expression.as_ref(),
+            _ => self,
+        }
+    }
 }
 
 fn model_inner_type(model: &Expression) -> Type {
@@ -1738,5 +1755,10 @@ pub fn pretty_print(f: &mut dyn std::fmt::Write, expression: &Expression) -> std
             write!(f, ")")
         }
         Expression::EmptyComponentFactory => write!(f, "<empty-component-factory>"),
+        Expression::DebugHook { expression, id } => {
+            write!(f, "debug-hook(")?;
+            pretty_print(f, expression)?;
+            write!(f, "\"{id}\")")
+        }
     }
 }
diff --git a/internal/compiler/lib.rs b/internal/compiler/lib.rs
index 66077891d28..97b2c1dd9c5 100644
--- a/internal/compiler/lib.rs
+++ b/internal/compiler/lib.rs
@@ -148,6 +148,9 @@ pub struct CompilerConfiguration {
     /// Generate debug information for elements (ids, type names)
     pub debug_info: bool,
 
+    /// Generate debug hooks to inspect/override properties.
+    pub debug_hooks: Option<std::hash::RandomState>,
+
     pub components_to_generate: ComponentSelection,
 
     #[cfg(feature = "software-renderer")]
@@ -226,6 +229,7 @@ impl CompilerConfiguration {
             translation_domain: None,
             cpp_namespace,
             debug_info,
+            debug_hooks: None,
             components_to_generate: ComponentSelection::ExportedWindows,
             #[cfg(feature = "software-renderer")]
             font_cache: Default::default(),
diff --git a/internal/compiler/llr/lower_expression.rs b/internal/compiler/llr/lower_expression.rs
index 38e89984276..0400ced059f 100644
--- a/internal/compiler/llr/lower_expression.rs
+++ b/internal/compiler/llr/lower_expression.rs
@@ -252,6 +252,7 @@ pub fn lower_expression(
             rhs: Box::new(lower_expression(rhs, ctx)),
         },
         tree_Expression::EmptyComponentFactory => llr_Expression::EmptyComponentFactory,
+        tree_Expression::DebugHook { expression, .. } => lower_expression(expression, ctx),
     }
 }
 
diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs
index 869c5e7ee39..b394115333a 100644
--- a/internal/compiler/object_tree.rs
+++ b/internal/compiler/object_tree.rs
@@ -641,6 +641,11 @@ pub struct ElementDebugInfo {
     // The id qualified with the enclosing component name. Given `foo := Bar {}` this is `EnclosingComponent::foo`
     pub qualified_id: Option<SmolStr>,
     pub type_name: String,
+    // Hold an id for each element that is unique during this build, based on the source file and
+    // the offset of the `LBrace` token.
+    //
+    // This helps to cross-reference the element in the different build stages the LSP has to deal with.
+    pub element_hash: u64,
     pub node: syntax_nodes::Element,
     // Field to indicate whether this element was a layout that had
     // been lowered into a rectangle in the lower_layouts pass.
@@ -997,6 +1002,7 @@ impl Element {
             base_type,
             debug: vec![ElementDebugInfo {
                 qualified_id,
+                element_hash: 0,
                 type_name,
                 node: node.clone(),
                 layout: None,
diff --git a/internal/compiler/passes.rs b/internal/compiler/passes.rs
index af1ad585b2d..cfa810d603b 100644
--- a/internal/compiler/passes.rs
+++ b/internal/compiler/passes.rs
@@ -25,6 +25,7 @@ mod flickable;
 mod focus_handling;
 pub mod generate_item_indices;
 pub mod infer_aliases_types;
+mod inject_debug_hooks;
 mod inlining;
 mod lower_absolute_coordinates;
 mod lower_accessibility;
@@ -56,6 +57,16 @@ use crate::expression_tree::Expression;
 use crate::namedreference::NamedReference;
 use smol_str::SmolStr;
 
+pub fn ignore_debug_hooks(expr: &Expression) -> &Expression {
+    let mut expr = expr;
+    loop {
+        match expr {
+            Expression::DebugHook { expression, .. } => expr = expression.as_ref(),
+            _ => return expr,
+        }
+    }
+}
+
 pub async fn run_passes(
     doc: &mut crate::object_tree::Document,
     type_loader: &mut crate::typeloader::TypeLoader,
@@ -81,6 +92,7 @@ pub async fn run_passes(
     };
 
     let global_type_registry = type_loader.global_type_registry.clone();
+
     run_import_passes(doc, type_loader, diag);
     check_public_api::check_public_api(doc, &type_loader.compiler_config, diag);
 
@@ -313,6 +325,7 @@ pub fn run_import_passes(
     type_loader: &crate::typeloader::TypeLoader,
     diag: &mut crate::diagnostics::BuildDiagnostics,
 ) {
+    inject_debug_hooks::inject_debug_hooks(doc, type_loader);
     infer_aliases_types::resolve_aliases(doc, diag);
     resolving::resolve_expressions(doc, type_loader, diag);
     purity_check::purity_check(doc, diag);
diff --git a/internal/compiler/passes/focus_handling.rs b/internal/compiler/passes/focus_handling.rs
index dd54dda799b..51e596ac703 100644
--- a/internal/compiler/passes/focus_handling.rs
+++ b/internal/compiler/passes/focus_handling.rs
@@ -82,7 +82,8 @@ impl<'a> LocalFocusForwards<'a> {
                 return;
             };
 
-            let Expression::ElementReference(focus_target) = &forward_focus_binding.expression
+            let Expression::ElementReference(focus_target) =
+                super::ignore_debug_hooks(&forward_focus_binding.expression)
             else {
                 // resolve expressions pass has produced type errors
                 debug_assert!(diag.has_errors());
diff --git a/internal/compiler/passes/infer_aliases_types.rs b/internal/compiler/passes/infer_aliases_types.rs
index c7b43733a1d..a682a9870db 100644
--- a/internal/compiler/passes/infer_aliases_types.rs
+++ b/internal/compiler/passes/infer_aliases_types.rs
@@ -82,7 +82,7 @@ fn resolve_alias(
         assert!(diag.has_errors());
         return;
     };
-    let nr = match &binding.borrow().expression {
+    let nr = match super::ignore_debug_hooks(&binding.borrow().expression) {
         Expression::Uncompiled(node) => {
             let Some(node) = syntax_nodes::TwoWayBinding::new(node.clone()) else {
                 assert!(
diff --git a/internal/compiler/passes/inject_debug_hooks.rs b/internal/compiler/passes/inject_debug_hooks.rs
new file mode 100644
index 00000000000..7ad8dab8082
--- /dev/null
+++ b/internal/compiler/passes/inject_debug_hooks.rs
@@ -0,0 +1,72 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+//! Hooks properties for live inspection.
+
+use crate::{expression_tree, object_tree, typeloader};
+
+pub fn inject_debug_hooks(doc: &object_tree::Document, type_loader: &typeloader::TypeLoader) {
+    let Some(random_state) = &type_loader.compiler_config.debug_hooks else {
+        return;
+    };
+
+    doc.visit_all_used_components(|component| {
+        object_tree::recurse_elem_including_sub_components(component, &(), &mut |e, &()| {
+            process_element(e, random_state);
+        })
+    });
+}
+
+fn property_id(element_id: u64, name: &smol_str::SmolStr) -> smol_str::SmolStr {
+    smol_str::format_smolstr!("?{element_id}-{name}")
+}
+
+fn calculate_element_hash(
+    elem: &object_tree::Element,
+    random_state: &std::hash::RandomState,
+) -> u64 {
+    let node = &elem.debug.first().expect("There was one element a moment ago").node;
+
+    let elem_path = node.source_file.path();
+    let elem_offset = node
+        .child_token(crate::parser::SyntaxKind::LBrace)
+        .expect("All elements have a opening Brace")
+        .text_range()
+        .start();
+
+    use std::hash::{BuildHasher, Hasher};
+    let mut hasher = random_state.build_hasher();
+    hasher.write(elem_path.as_os_str().as_encoded_bytes());
+    hasher.write_u32(elem_offset.into());
+    hasher.finish()
+}
+
+fn process_element(element: &object_tree::ElementRc, random_state: &std::hash::RandomState) {
+    let mut elem = element.borrow_mut();
+    // We did not merge Elements yet and we have debug info!
+    assert_eq!(elem.debug.len(), 1);
+
+    // Ignore nodes previously set up
+    if elem.debug.first().expect("There was one element a moment ago").element_hash != 0 {
+        return;
+    }
+
+    let element_hash = calculate_element_hash(&elem, random_state);
+
+    elem.bindings.iter().for_each(|(name, be)| {
+        let expr = std::mem::take(&mut be.borrow_mut().expression);
+        be.borrow_mut().expression = {
+            let stripped = super::ignore_debug_hooks(&expr);
+            if matches!(stripped, expression_tree::Expression::Invalid) {
+                stripped.clone()
+            } else {
+                expression_tree::Expression::DebugHook {
+                    expression: Box::new(expr),
+                    id: property_id(element_hash, name),
+                }
+            }
+        };
+    });
+
+    elem.debug.first_mut().expect("There was one element a moment ago").element_hash = element_hash;
+}
diff --git a/internal/compiler/passes/lower_accessibility.rs b/internal/compiler/passes/lower_accessibility.rs
index 767a3c805a3..959c81d08f6 100644
--- a/internal/compiler/passes/lower_accessibility.rs
+++ b/internal/compiler/passes/lower_accessibility.rs
@@ -22,7 +22,9 @@ pub fn lower_accessibility_properties(component: &Rc<Component>, diag: &mut Buil
             apply_builtin(elem);
             let accessible_role_set = match elem.borrow().bindings.get("accessible-role") {
                 Some(role) => {
-                    if let Expression::EnumerationValue(val) = &role.borrow().expression {
+                    if let Expression::EnumerationValue(val) =
+                        super::ignore_debug_hooks(&role.borrow().expression)
+                    {
                         debug_assert_eq!(val.enumeration.name, "AccessibleRole");
                         debug_assert_eq!(val.enumeration.values[0], "none");
                         if val.value == 0 {
diff --git a/internal/compiler/passes/lower_layout.rs b/internal/compiler/passes/lower_layout.rs
index 141c611a212..ebcadc12e41 100644
--- a/internal/compiler/passes/lower_layout.rs
+++ b/internal/compiler/passes/lower_layout.rs
@@ -60,7 +60,8 @@ pub fn lower_layouts(
 fn check_preferred_size_100(elem: &ElementRc, prop: &str, diag: &mut BuildDiagnostics) -> bool {
     let ret = if let Some(p) = elem.borrow().bindings.get(prop) {
         if p.borrow().expression.ty() == Type::Percent {
-            if !matches!(p.borrow().expression, Expression::NumberLiteral(val, _) if val == 100.) {
+            if !matches!(p.borrow().expression.ignore_debug_hooks(), Expression::NumberLiteral(val, _) if *val == 100.)
+            {
                 diag.push_error(
                     format!("{prop} must either be a length, or the literal '100%'"),
                     &*p.borrow(),
@@ -523,7 +524,9 @@ fn lower_dialog_layout(
             layout_child.borrow_mut().bindings.remove("dialog-button-role");
         let is_button = if let Some(role_binding) = dialog_button_role_binding {
             let role_binding = role_binding.into_inner();
-            if let Expression::EnumerationValue(val) = &role_binding.expression {
+            if let Expression::EnumerationValue(val) =
+                super::ignore_debug_hooks(&role_binding.expression)
+            {
                 let en = &val.enumeration;
                 debug_assert_eq!(en.name, "DialogButtonRole");
                 button_roles.push(en.values[val.value].clone());
@@ -550,7 +553,9 @@ fn lower_dialog_layout(
                 ),
                 Some(binding) => {
                     let binding = &*binding.borrow();
-                    if let Expression::EnumerationValue(val) = &binding.expression {
+                    if let Expression::EnumerationValue(val) =
+                        super::ignore_debug_hooks(&binding.expression)
+                    {
                         let en = &val.enumeration;
                         debug_assert_eq!(en.name, "StandardButtonKind");
                         let kind = &en.values[val.value];
@@ -583,7 +588,7 @@ fn lower_dialog_layout(
                             let clicked_ty =
                                 layout_child.borrow().lookup_property("clicked").property_type;
                             if matches!(&clicked_ty, Type::Callback { .. })
-                                && layout_child.borrow().bindings.get("clicked").map_or(true, |c| {
+                                && layout_child.borrow().bindings.get("clicked").is_none_or(|c| {
                                     matches!(c.borrow().expression, Expression::Invalid)
                                 })
                             {
@@ -798,7 +803,7 @@ fn eval_const_expr(
     span: &dyn crate::diagnostics::Spanned,
     diag: &mut BuildDiagnostics,
 ) -> Option<u16> {
-    match expression {
+    match super::ignore_debug_hooks(expression) {
         Expression::NumberLiteral(v, Unit::None) => {
             if *v < 0. || *v > u16::MAX as f64 || !v.trunc().approx_eq(v) {
                 diag.push_error(format!("'{name}' must be a positive integer"), span);
diff --git a/internal/compiler/passes/lower_popups.rs b/internal/compiler/passes/lower_popups.rs
index 1fb00641fd1..c6201ed3bf1 100644
--- a/internal/compiler/passes/lower_popups.rs
+++ b/internal/compiler/passes/lower_popups.rs
@@ -48,7 +48,7 @@ fn lower_popup_window(
     diag: &mut BuildDiagnostics,
 ) {
     if let Some(binding) = popup_window_element.borrow().bindings.get(CLOSE_ON_CLICK) {
-        if popup_window_element.borrow().bindings.get(CLOSE_POLICY).is_some() {
+        if popup_window_element.borrow().bindings.contains_key(CLOSE_POLICY) {
             diag.push_error(
                 "close-policy and close-on-click cannot be set at the same time".into(),
                 &binding.borrow().span,
@@ -59,12 +59,18 @@ fn lower_popup_window(
                 CLOSE_POLICY,
                 &binding.borrow().span,
             );
-            if !matches!(binding.borrow().expression, Expression::BoolLiteral(_)) {
+            if !matches!(
+                super::ignore_debug_hooks(&binding.borrow().expression),
+                Expression::BoolLiteral(_)
+            ) {
                 report_const_error(CLOSE_ON_CLICK, &binding.borrow().span, diag);
             }
         }
     } else if let Some(binding) = popup_window_element.borrow().bindings.get(CLOSE_POLICY) {
-        if !matches!(binding.borrow().expression, Expression::EnumerationValue(_)) {
+        if !matches!(
+            super::ignore_debug_hooks(&binding.borrow().expression),
+            Expression::EnumerationValue(_)
+        ) {
             report_const_error(CLOSE_POLICY, &binding.borrow().span, diag);
         }
     }
@@ -110,12 +116,12 @@ fn lower_popup_window(
     }
 
     let map_close_on_click_value = |b: &BindingExpression| {
-        let Expression::BoolLiteral(v) = b.expression else {
+        let Expression::BoolLiteral(v) = super::ignore_debug_hooks(&b.expression) else {
             assert!(diag.has_errors());
             return None;
         };
         let enum_ty = crate::typeregister::BUILTIN.with(|e| e.enums.PopupClosePolicy.clone());
-        let s = if v { "close-on-click" } else { "no-auto-close" };
+        let s = if *v { "close-on-click" } else { "no-auto-close" };
         Some(EnumerationValue {
             value: enum_ty.values.iter().position(|v| v == s).unwrap(),
             enumeration: enum_ty,
@@ -125,8 +131,8 @@ fn lower_popup_window(
     let close_policy =
         popup_window_element.borrow_mut().bindings.remove(CLOSE_POLICY).and_then(|b| {
             let b = b.into_inner();
-            if let Expression::EnumerationValue(v) = b.expression {
-                Some(v)
+            if let Expression::EnumerationValue(v) = super::ignore_debug_hooks(&b.expression) {
+                Some(v.clone())
             } else {
                 assert!(diag.has_errors());
                 None
diff --git a/internal/compiler/passes/remove_return.rs b/internal/compiler/passes/remove_return.rs
index 89e0d28c951..d5449c17dae 100644
--- a/internal/compiler/passes/remove_return.rs
+++ b/internal/compiler/passes/remove_return.rs
@@ -39,6 +39,9 @@ fn process_expression(
     ty: &Type,
 ) -> ExpressionResult {
     match e {
+        Expression::DebugHook { expression, .. } => {
+            process_expression(*expression, toplevel, ctx, ty)
+        }
         Expression::ReturnStatement(expr) => ExpressionResult::Return(expr.map(|e| *e)),
         Expression::CodeBlock(expr) => {
             process_codeblock(expr.into_iter().peekable(), toplevel, ty, ctx)
@@ -69,14 +72,14 @@ fn process_expression(
                 }
                 (ExpressionResult::Return(te), ExpressionResult::Return(fe)) => {
                     ExpressionResult::Return(Some(Expression::Condition {
-                        condition: condition.into(),
+                        condition,
                         true_expr: te.unwrap_or(Expression::CodeBlock(vec![])).into(),
                         false_expr: fe.unwrap_or(Expression::CodeBlock(vec![])).into(),
                     }))
                 }
                 (te, fe) => {
-                    let te = te.into_return_object(&ty, &ctx.ret_ty);
-                    let fe = fe.into_return_object(&ty, &ctx.ret_ty);
+                    let te = te.into_return_object(ty, &ctx.ret_ty);
+                    let fe = fe.into_return_object(ty, &ctx.ret_ty);
                     ExpressionResult::ReturnObject {
                         has_value: has_value(ty),
                         has_return_value: has_value(&ctx.ret_ty),
diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs
index 69b40845722..bacdbc57f32 100644
--- a/internal/compiler/passes/resolving.rs
+++ b/internal/compiler/passes/resolving.rs
@@ -35,7 +35,7 @@ fn resolve_expression(
     type_loader: &crate::typeloader::TypeLoader,
     diag: &mut BuildDiagnostics,
 ) {
-    if let Expression::Uncompiled(node) = expr {
+    if let Expression::Uncompiled(node) = expr.ignore_debug_hooks() {
         let mut lookup_ctx = LookupCtx {
             property_name,
             property_type,
@@ -77,7 +77,10 @@ fn resolve_expression(
                 Expression::Invalid
             }
         };
-        *expr = new_expr;
+        match expr {
+            Expression::DebugHook { expression, .. } => *expression = Box::new(new_expr),
+            _ => *expr = new_expr,
+        }
     }
 }
 
@@ -1600,7 +1603,9 @@ fn resolve_two_way_bindings(
             &mut |elem, scope| {
                 for (prop_name, binding) in &elem.borrow().bindings {
                     let mut binding = binding.borrow_mut();
-                    if let Expression::Uncompiled(node) = binding.expression.clone() {
+                    if let Expression::Uncompiled(node) =
+                        binding.expression.ignore_debug_hooks().clone()
+                    {
                         if let Some(n) = syntax_nodes::TwoWayBinding::new(node.clone()) {
                             let lhs_lookup = elem.borrow().lookup_property(prop_name);
                             if !lhs_lookup.is_valid() {
diff --git a/internal/compiler/passes/z_order.rs b/internal/compiler/passes/z_order.rs
index 406f0fda193..6643843b2a9 100644
--- a/internal/compiler/passes/z_order.rs
+++ b/internal/compiler/passes/z_order.rs
@@ -74,7 +74,7 @@ fn eval_const_expr(
     span: &dyn crate::diagnostics::Spanned,
     diag: &mut BuildDiagnostics,
 ) -> Option<f64> {
-    match expression {
+    match super::ignore_debug_hooks(expression) {
         Expression::NumberLiteral(v, Unit::None) => Some(*v),
         Expression::Cast { from, .. } => eval_const_expr(from, name, span, diag),
         Expression::UnaryOp { sub, op: '-' } => eval_const_expr(sub, name, span, diag).map(|v| -v),
diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs
index 2d9b043251e..570c0f4e443 100644
--- a/internal/interpreter/eval.rs
+++ b/internal/interpreter/eval.rs
@@ -395,7 +395,8 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon
                 MinMaxOp::Max => Value::Number(lhs.max(rhs)),
             }
         }
-        Expression::EmptyComponentFactory => Value::ComponentFactory(Default::default())
+        Expression::EmptyComponentFactory => Value::ComponentFactory(Default::default()),
+        Expression::DebugHook { expression, .. } => eval_expression(expression, local_context),
     }
 }
 


Reviews:
user: login: ogoffart
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T18:45:46Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T20:11:34Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:54:05Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:54:37Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:56:49Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:58:31Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:00:00Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:03:36Z
user: login: ogoffart
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T07:41:09Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T15:38:00Z
user: login: hunger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T15:38:35Z
user: login: ogoffart
body: I wouldn't pass a `RandomState` to compute hashes. Instead of putting the full filename in the `property_id`, or perhaps an unique file_id computed before and stored in the `SourceFileInner`
And then there is no need to store the element_hash in the debug info as it can trivially be re-computed from the node.

Anyway, in the interest of moving this forward, let's merge this anyway. It can always be changed later.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T09:24:16Z

Review comments:
diff_hunk: @@ -1108,6 +1116,7 @@ impl Expression {
             Expression::SolveLayout(..) => false,
             Expression::MinMax { lhs, rhs, .. } => lhs.is_constant() && rhs.is_constant(),
             Expression::EmptyComponentFactory => true,
+            Expression::DebugHook { .. } => false, // We want to be able to override this at any point in time!
path: internal/compiler/expression_tree.rs
user: login: ogoffart
body: ```suggestion
            Expression::DebugHook { .. } => false,
```

False is correct.

(if you want to put a comment, put it on the previous line like for the other expressions.)
created_at: 2025-04-03T18:33:56Z
author_association: MEMBER
diff_hunk: @@ -252,6 +252,10 @@ pub fn lower_expression(
             rhs: Box::new(lower_expression(rhs, ctx)),
         },
         tree_Expression::EmptyComponentFactory => llr_Expression::EmptyComponentFactory,
+        tree_Expression::DebugHook { expression, .. } => {
+            // Ignore debug-hoooks in LLR for now!
path: internal/compiler/llr/lower_expression.rs
user: login: ogoffart
body: ```suggestion
```

No need to scream it ;-)  (the "for now!" is over the top and doesn't say much. Until when?)
created_at: 2025-04-03T18:36:30Z
author_association: MEMBER
diff_hunk: @@ -641,6 +641,7 @@ pub struct ElementDebugInfo {
     // The id qualified with the enclosing component name. Given `foo := Bar {}` this is `EnclosingComponent::foo`
     pub qualified_id: Option<SmolStr>,
     pub type_name: String,
+    pub element_id: u64,
path: internal/compiler/object_tree.rs
user: login: ogoffart
body: what's that element_id for?
Could you add a comment describing what it is and what it is for?
created_at: 2025-04-03T18:37:59Z
author_association: MEMBER
diff_hunk: @@ -77,7 +77,11 @@ fn resolve_expression(
                 Expression::Invalid
             }
         };
-        *expr = new_expr;
+        match expr {
+            // TODO: Handle nested debug hooks? Probably not going to happen anyway...
path: internal/compiler/passes/resolving.rs
user: login: ogoffart
body: Maybe add an assert?
created_at: 2025-04-03T18:42:34Z
author_association: MEMBER
diff_hunk: @@ -1600,7 +1604,9 @@ fn resolve_two_way_bindings(
             &mut |elem, scope| {
                 for (prop_name, binding) in &elem.borrow().bindings {
                     let mut binding = binding.borrow_mut();
-                    if let Expression::Uncompiled(node) = binding.expression.clone() {
+                    if let Expression::Uncompiled(node) =
+                        super::ignore_debug_hooks(&binding.expression.clone())
path: internal/compiler/passes/resolving.rs
user: login: ogoffart
body: taking the reference of a clone() looks strange.
created_at: 2025-04-03T18:44:03Z
author_association: MEMBER
diff_hunk: @@ -81,6 +92,9 @@ pub async fn run_passes(
     };
 
     let global_type_registry = type_loader.global_type_registry.clone();
+
+    inject_debug_hooks::inject_debug_hooks(doc, type_loader);
path: internal/compiler/passes.rs
user: login: ogoffart
body: I think this should go in run_import_passes, if you want to have it on anything else than the main component.
created_at: 2025-04-03T18:45:06Z
author_association: MEMBER
diff_hunk: @@ -56,6 +57,16 @@ use crate::expression_tree::Expression;
 use crate::namedreference::NamedReference;
 use smol_str::SmolStr;
 
+pub fn ignore_debug_hooks(expr: &Expression) -> &Expression {
path: internal/compiler/passes.rs
user: login: ogoffart
body: Not sure if this belong in this module.

Maybe in `impl Expression` ?
created_at: 2025-04-03T18:45:40Z
author_association: MEMBER
diff_hunk: @@ -77,7 +77,11 @@ fn resolve_expression(
                 Expression::Invalid
             }
         };
-        *expr = new_expr;
+        match expr {
+            // TODO: Handle nested debug hooks? Probably not going to happen anyway...
path: internal/compiler/passes/resolving.rs
user: login: hunger
body: Actually I need to remove that: They can not be nested now that I removed the parser that allowed users to create debug hooks.
created_at: 2025-04-04T20:11:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1108,6 +1116,7 @@ impl Expression {
             Expression::SolveLayout(..) => false,
             Expression::MinMax { lhs, rhs, .. } => lhs.is_constant() && rhs.is_constant(),
             Expression::EmptyComponentFactory => true,
+            Expression::DebugHook { .. } => false, // We want to be able to override this at any point in time!
path: internal/compiler/expression_tree.rs
user: login: hunger
body: Will be gone in the next push.
created_at: 2025-04-07T14:54:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -252,6 +252,10 @@ pub fn lower_expression(
             rhs: Box::new(lower_expression(rhs, ctx)),
         },
         tree_Expression::EmptyComponentFactory => llr_Expression::EmptyComponentFactory,
+        tree_Expression::DebugHook { expression, .. } => {
+            // Ignore debug-hoooks in LLR for now!
path: internal/compiler/llr/lower_expression.rs
user: login: hunger
body: Will be gone in the next push.
created_at: 2025-04-07T14:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -641,6 +641,7 @@ pub struct ElementDebugInfo {
     // The id qualified with the enclosing component name. Given `foo := Bar {}` this is `EnclosingComponent::foo`
     pub qualified_id: Option<SmolStr>,
     pub type_name: String,
+    pub element_id: u64,
path: internal/compiler/object_tree.rs
user: login: hunger
body: Comment added in the next push.
created_at: 2025-04-07T14:56:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1600,7 +1604,9 @@ fn resolve_two_way_bindings(
             &mut |elem, scope| {
                 for (prop_name, binding) in &elem.borrow().bindings {
                     let mut binding = binding.borrow_mut();
-                    if let Expression::Uncompiled(node) = binding.expression.clone() {
+                    if let Expression::Uncompiled(node) =
+                        super::ignore_debug_hooks(&binding.expression.clone())
path: internal/compiler/passes/resolving.rs
user: login: hunger
body: Good catch, thanks!
created_at: 2025-04-07T14:58:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +92,9 @@ pub async fn run_passes(
     };
 
     let global_type_registry = type_loader.global_type_registry.clone();
+
+    inject_debug_hooks::inject_debug_hooks(doc, type_loader);
path: internal/compiler/passes.rs
user: login: hunger
body: But that way this ends up getting called several times, doesn't it?

That would mean I have to avoid instrumenting expressions several times.
created_at: 2025-04-07T15:00:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,6 +57,16 @@ use crate::expression_tree::Expression;
 use crate::namedreference::NamedReference;
 use smol_str::SmolStr;
 
+pub fn ignore_debug_hooks(expr: &Expression) -> &Expression {
path: internal/compiler/passes.rs
user: login: hunger
body: This will be moved in the next push.
created_at: 2025-04-07T15:03:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +92,9 @@ pub async fn run_passes(
     };
 
     let global_type_registry = type_loader.global_type_registry.clone();
+
+    inject_debug_hooks::inject_debug_hooks(doc, type_loader);
path: internal/compiler/passes.rs
user: login: ogoffart
body: Yes that's right, but that'd be on different documents. So it would be only called once per file, for the file that is used.

Otherwise, you need to run that pass after  `collect_subcomponents` in order to also mark component declared in other files.
created_at: 2025-04-08T07:21:47Z
author_association: MEMBER
diff_hunk: @@ -207,6 +207,7 @@ fn simplify_expression(expr: &mut Expression) -> bool {
         Expression::LayoutCacheAccess { .. } => false,
         Expression::SolveLayout { .. } => false,
         Expression::ComputeLayoutInfo { .. } => false,
+        Expression::DebugHook { .. } => false, // This is not const by design
path: internal/compiler/passes/const_propagation.rs
user: login: ogoffart
body: The return value is actually whether this contains a referenced element, not if it is const.

The inner expression can still be simplified, so the fall trough seems correct.

Or why did you add this?
created_at: 2025-04-08T07:25:40Z
author_association: MEMBER
diff_hunk: @@ -81,6 +92,9 @@ pub async fn run_passes(
     };
 
     let global_type_registry = type_loader.global_type_registry.clone();
+
+    inject_debug_hooks::inject_debug_hooks(doc, type_loader);
path: internal/compiler/passes.rs
user: login: hunger
body: I added my pass into `run_import_passes` and added a check to prevent the debug hooks getting added twice.
created_at: 2025-04-08T15:37:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,6 +207,7 @@ fn simplify_expression(expr: &mut Expression) -> bool {
         Expression::LayoutCacheAccess { .. } => false,
         Expression::SolveLayout { .. } => false,
         Expression::ComputeLayoutInfo { .. } => false,
+        Expression::DebugHook { .. } => false, // This is not const by design
path: internal/compiler/passes/const_propagation.rs
user: login: hunger
body: <That's gone now. I misunderstood what this does.
created_at: 2025-04-08T15:38:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: hunger
created_at: 2025-04-03T17:15:43Z
body: Tests pass now, so let's remove some fluff from my original branch and get it merged :-)

This is just the compiler side of things, I have left the interpreter alone as far as I could as we were discussing another API for that and I want to test that out first.
author_association: CONTRIBUTOR
user: login: ogoffart
created_at: 2025-04-04T04:40:29Z
body: (general comment unrelated to this particular change)

You notice I ask you to remove some comments and add others. My guideline for comments is that we should document the interface. This means adding documentation for fields and functions, especially public ones, but also for internal ones. (Unless the name is good enough that there is nothing to add.) However, the code itself only needs a comment if it's not obvious. In that case, explain why, not what. Otherwise, it just adds noise. Also, comments should not be temporary or tied to specific changes. The reasoning behind a change should be in the commit message. Comments like 'for now' don’t age well when reading the code after a few years
author_association: MEMBER
user: login: hunger
created_at: 2025-04-08T15:39:32Z
body: All the changes are in the SQUASH commit on top.

This includes a hash-based eleemnt-id, based on a random state passed in via the `CompilerConfiguration`.
author_association: CONTRIBUTOR
user: login: hunger
created_at: 2025-04-08T16:03:00Z
body: Semi-unrelated question: Why are the `run_import_passes` run twice? If I see that correctly, the typeloader runs them for each document it loads and in the end they are run again when the complete passes are run on the main file.
author_association: CONTRIBUTOR
user: login: ogoffart
created_at: 2025-04-08T18:32:19Z
body: > Semi-unrelated question: Why are the run_import_passes run twice? 

looks like a bug, it is not supposed to be run twice.
I think the import_pass are not run on the main document since that document is not loaded from the importer. But maybe this has changed. 
author_association: MEMBER
user: login: hunger
created_at: 2025-04-09T11:51:18Z
body: Yes, that could be done, but I'd be touching way more code then.

I particularly like the file_id: If we used a file_id in the compiler consistently, then we could map the Path/Url to file_ids outside the compiler and that way get rid of all the conversions we do in the LSP right now.
author_association: CONTRIBUTOR