1use crate::expression_tree::Expression;
23use crate::langtype::PropertyLookupMode;
24use crate::object_tree::forward_inherited_expression::{
25 ForwardedReferenceCache, InheritedExpression, forward_inherited_expression,
26};
27use crate::object_tree::{self, Element, ElementDebugInfo, ElementRc, PropertyVisibility};
28use crate::symbol_counters::SymbolCounters;
29use std::rc::Rc;
30
31pub fn inject_debug_hooks(
32 root_components: &[Rc<object_tree::Component>],
33 random_state: &std::hash::RandomState,
34 symbol_counters: &SymbolCounters,
35 forwarded_references: &mut ForwardedReferenceCache,
36) {
37 for component in root_components {
38 let root = component.root_element.clone();
39 object_tree::recurse_elem(&root, &(), &mut |element, &()| {
40 process_existing_bindings(element, random_state);
41 });
42 }
43
44 for component in root_components {
49 let root = component.root_element.clone();
50 object_tree::recurse_elem(&root, &(), &mut |element, &()| {
51 let is_root = Rc::ptr_eq(element, &root);
52 process_missing_bindings(element, symbol_counters, forwarded_references, is_root);
53 });
54 }
55}
56
57pub fn property_id(element_id: u64, name: &smol_str::SmolStr) -> smol_str::SmolStr {
58 smol_str::format_smolstr!("?{element_id}-{name}")
59}
60
61pub fn validate_no_orphan_synthetic_hooks(component: &std::rc::Rc<object_tree::Component>) {
69 if !cfg!(debug_assertions) {
70 return;
71 }
72 object_tree::recurse_elem_including_sub_components_no_borrow(
73 component,
74 &(),
75 &mut |elem, &()| {
76 let elem = elem.borrow();
77 for (name, binding_expression) in elem.bindings_including_synthetic() {
78 if !binding_expression.borrow().expression.is_synthetic_debug_hook() {
79 continue;
80 }
81 if super::materialize_fake_properties::should_materialize(
82 &elem.property_declarations,
83 &elem.base_type,
84 name,
85 )
86 .is_some()
87 {
88 panic!(
89 "Orphan synthetic debug hook: property '{name}' on element '{}' ({}) does \
90 not exist at runtime — a pass inserted or kept a synthetic hook for a \
91 property that is neither native, declared, nor materialized",
92 elem.id,
93 elem.debug
94 .first()
95 .map(|d| format!("{:?}", d.node.source_file.path()))
96 .unwrap_or_default(),
97 );
98 }
99 }
100 },
101 );
102}
103
104fn calculate_element_hash(
105 debug_info: &ElementDebugInfo,
106 random_state: &std::hash::RandomState,
107) -> u64 {
108 let node = &debug_info.node;
110
111 let elem_path = node.source_file.path();
112 let elem_offset = node
113 .child_token(crate::parser::SyntaxKind::LBrace)
114 .expect("All elements have a opening Brace")
115 .text_range()
116 .start();
117
118 use std::hash::{BuildHasher, Hasher};
119 let mut hasher = random_state.build_hasher();
120 hasher.write(elem_path.as_os_str().as_encoded_bytes());
121 hasher.write_u32(elem_offset.into());
122 hasher.finish()
123}
124
125fn assign_element_hash(element: &ElementRc, random_state: &std::hash::RandomState) -> u64 {
126 let mut elem = element.borrow_mut();
127
128 debug_assert!(elem.debug.len() == 1);
132 let debug_info = &mut elem.debug[0];
133 if debug_info.element_hash == 0 {
134 let hash = calculate_element_hash(debug_info, random_state);
135 debug_info.element_hash = hash;
136 }
137 debug_info.element_hash
138}
139
140fn hook_existing_bindings(element: &ElementRc, element_hash: u64) {
141 let elem = element.borrow();
142 elem.bindings_including_synthetic().for_each(|(name, be)| {
143 if !elem
147 .lookup_property(name, PropertyLookupMode::InternalName)
148 .property_type
149 .is_property_type()
150 {
151 return;
152 }
153 let expr = std::mem::take(&mut be.borrow_mut().expression);
154 be.borrow_mut().expression = {
155 let stripped = expr.ignore_debug_hooks();
156 if matches!(stripped, Expression::Invalid)
157 || matches!(expr, Expression::DebugHook { .. })
158 {
159 expr
160 } else {
161 Expression::DebugHook {
162 expression: Box::new(expr),
163 id: property_id(element_hash, name),
164 synthetic: false,
165 }
166 }
167 };
168 });
169}
170
171fn property_defaults(
172 elem: &Element,
173) -> impl Iterator<Item = (smol_str::SmolStr, crate::expression_tree::Expression, bool)> {
174 let base_props = elem.base_type.property_list();
176
177 let own_props: Vec<(smol_str::SmolStr, crate::langtype::Type)> = elem
179 .property_declarations
180 .iter()
181 .map(|(name, decl)| (name.clone(), decl.property_type.clone()))
182 .collect();
183
184 base_props
185 .into_iter()
186 .chain(own_props)
187 .filter(|(name, _)| elem.binding_cell_including_synthetic(name.as_str()).is_none())
188 .filter_map(|(name, _ty)| {
189 let name_str = name.clone();
190 let lookup = elem.lookup_property(&name_str, PropertyLookupMode::InternalName);
191 if lookup.builtin_function.is_some() {
193 return None;
194 }
195 match lookup.property_visibility {
197 PropertyVisibility::Public
198 | PropertyVisibility::InOut
199 | PropertyVisibility::Input
200 | PropertyVisibility::Private => {}
201 PropertyVisibility::Output
202 | PropertyVisibility::Constexpr
203 | PropertyVisibility::Protected
204 | PropertyVisibility::Fake => return None,
205 }
206 let default = Expression::default_value_for_type(&lookup.property_type);
207 if matches!(default, Expression::Invalid) {
208 return None;
209 }
210 Some((name, default, true))
211 })
212}
213
214fn geometry_properties()
219-> impl Iterator<Item = (smol_str::SmolStr, crate::expression_tree::Expression, bool)> {
220 crate::typeregister::RESERVED_GEOMETRY_PROPERTIES
221 .iter()
222 .filter(|(name, _)| *name != "z")
223 .filter_map(|(prop_name, ty)| {
224 let default = Expression::default_value_for_type(ty);
225 if matches!(default, Expression::Invalid) {
226 return None;
227 }
228 Some((smol_str::SmolStr::new_static(prop_name), default, true))
229 })
230}
231
232fn transform_properties<'a>(
238 element: &'a ElementRc,
239) -> impl Iterator<Item = (smol_str::SmolStr, crate::expression_tree::Expression, bool)> + 'a {
240 const TRANSFORM_PROPS: [&str; 3] =
242 ["transform-rotation", "transform-scale-x", "transform-scale-y"];
243 TRANSFORM_PROPS.into_iter().map(|property_name| {
244 let property_name = smol_str::SmolStr::new_static(property_name);
245 let default_expression =
246 super::lower_property_to_element::transform_property_default_value(
247 element,
248 &property_name,
249 )
250 .unwrap();
251 (property_name.clone(), default_expression, false)
252 })
253}
254
255fn add_hooks_for_non_existent_bindings(
256 element: &ElementRc,
257 element_hash: u64,
258 symbol_counters: &SymbolCounters,
259 forwarded_references: &mut ForwardedReferenceCache,
260 is_root: bool,
261) {
262 let elem = element.borrow();
263 let mut properties: Vec<_> = property_defaults(&elem).collect();
264
265 let becomes_root =
272 is_root || element.borrow().builtin_type().is_some_and(|b| b.name == "PopupWindow");
273
274 if !becomes_root {
278 properties.extend(geometry_properties());
279 };
280
281 if !becomes_root {
286 properties.extend(transform_properties(element));
287 }
288
289 drop(elem);
290 let unbound_properties = properties.into_iter().filter(|(name, _default, _synthetic)| {
291 let elem = element.borrow();
292 elem.binding_cell_including_synthetic(name).is_none()
293 && elem.lookup_property(name, PropertyLookupMode::InternalName).property_type != crate::langtype::Type::Invalid
295 && !elem.is_property_target_of_two_way_binding(name)
296 });
297
298 for (name, default_expression, synthetic) in unbound_properties {
299 let expression = match forward_inherited_expression(
300 element,
301 &name,
302 symbol_counters,
303 forwarded_references,
304 ) {
305 InheritedExpression::Expression(expression) => expression.ignore_debug_hooks().clone(),
306 InheritedExpression::TwoWayBinding => continue,
307 InheritedExpression::Unbound => default_expression,
308 };
309 let id = property_id(element_hash, &name);
310 let mut binding: crate::expression_tree::BindingExpression =
311 Expression::DebugHook { expression: Box::new(expression), id, synthetic }.into();
312 binding.priority = 0;
313 let mut elem = element.borrow_mut();
314 if elem.binding_cell_including_synthetic(&name).is_none() {
315 elem.set_binding(name, binding);
316 }
317 }
318}
319
320fn is_hookable(element: &ElementRc) -> bool {
321 let element = element.borrow();
322 if element.is_component_placeholder {
324 return false;
325 }
326 if element.debug.is_empty() {
327 return false;
328 }
329
330 true
331}
332
333fn process_existing_bindings(element: &ElementRc, random_state: &std::hash::RandomState) {
334 if !is_hookable(element) {
335 return;
336 }
337
338 let element_hash = assign_element_hash(element, random_state);
339
340 hook_existing_bindings(element, element_hash);
341}
342
343fn process_missing_bindings(
344 element: &ElementRc,
345 symbol_counters: &SymbolCounters,
346 forwarded_references: &mut ForwardedReferenceCache,
347 is_root: bool,
348) {
349 if !is_hookable(element) {
350 return;
351 }
352
353 let element_hash = element.borrow().debug[0].element_hash;
354 debug_assert_ne!(element_hash, 0);
355
356 add_hooks_for_non_existent_bindings(
357 element,
358 element_hash,
359 symbol_counters,
360 forwarded_references,
361 is_root,
362 );
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::object_tree::Component;
369 use std::rc::Rc;
370
371 fn compile(source: &str) -> crate::object_tree::Document {
372 let mut config =
373 crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
374 config.style = Some("fluent".into());
375 config.debug_hooks = Some(std::hash::RandomState::new());
376 config.inline_all_elements = false;
377 let mut diags = crate::diagnostics::BuildDiagnostics::default();
378 let doc_node = crate::parser::parse(
379 source.into(),
380 Some(std::path::Path::new("test.slint")),
381 &mut diags,
382 );
383 let (doc, diag, _) = spin_on::spin_on(crate::compile_syntax_node(doc_node, diags, config));
384 assert!(!diag.has_errors(), "{:?}", diag.to_string_vec());
385 doc
386 }
387
388 fn component<'a>(doc: &'a crate::object_tree::Document, id: &str) -> Rc<Component> {
389 doc.inner_components.iter().find(|c| c.id == id).expect("component").clone()
390 }
391
392 fn child(root: &ElementRc, id: &str) -> ElementRc {
393 fn rec(e: &ElementRc, id: &str) -> Option<ElementRc> {
396 let this_id = e.borrow().id.clone();
397 let matches = this_id == id
398 || this_id
399 .strip_prefix(&format!("{id}-"))
400 .is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit()));
401 if matches {
402 return Some(e.clone());
403 }
404 let children = e.borrow().children.clone();
405 children.iter().find_map(|c| rec(c, id))
406 }
407 rec(root, id).unwrap_or_else(|| panic!("element {id} not found"))
408 }
409
410 fn hooked(elem: &ElementRc, name: &str) -> Option<Expression> {
412 let e = elem.borrow();
413 let be = e.binding_cell_including_synthetic(name)?;
414 match be.borrow().expression.clone() {
415 Expression::DebugHook { expression, .. } => Some(*expression),
416 _ => None,
417 }
418 }
419
420 fn is_synthetic(elem: &ElementRc, name: &str) -> bool {
422 let e = elem.borrow();
423 let Some(be) = e.binding_cell_including_synthetic(name) else { return false };
424 matches!(be.borrow().expression, Expression::DebugHook { synthetic: true, .. })
425 }
426
427 #[test]
428 fn injects_and_wraps_top_level_only() {
429 let doc = compile(
430 r#"
431 component Sub inherits Rectangle {
432 inner-text := Text { }
433 }
434 export component Foo inherits Window {
435 txt := Text { }
436 rect := Rectangle { background: red; }
437 sub := Sub { }
438 }
439 "#,
440 );
441
442 let foo = component(&doc, "Foo");
443 let txt = child(&foo.root_element, "txt");
444 let rect = child(&foo.root_element, "rect");
445
446 let text_inner = hooked(&txt, "text").expect("txt.text should be a DebugHook");
448 assert!(is_synthetic(&txt, "text"), "txt.text hook should be synthetic (unbound property)");
449 assert!(
450 matches!(text_inner.ignore_debug_hooks(), Expression::StringLiteral(s) if s.is_empty()),
451 "txt.text default should be the empty-string sentinel, got {text_inner:?}"
452 );
453
454 let fs_inner = hooked(&txt, "font-size").expect("txt.font-size should be a DebugHook");
456 assert!(is_synthetic(&txt, "font-size"), "txt.font-size hook should be synthetic");
457 assert!(
458 matches!(fs_inner.ignore_debug_hooks(), Expression::NumberLiteral(v, _) if *v == 0.),
459 "txt.font-size default should be the 0 sentinel, got {fs_inner:?}"
460 );
461
462 let bg_inner = hooked(&rect, "background").expect("rect.background should be a DebugHook");
464 assert!(
465 !is_synthetic(&rect, "background"),
466 "rect.background should be non-synthetic (was explicitly set)"
467 );
468 assert!(
469 !matches!(bg_inner.ignore_debug_hooks(), Expression::Invalid),
470 "rect.background should wrap its real value"
471 );
472
473 assert_ne!(txt.borrow().debug.first().unwrap().element_hash, 0);
475 }
476
477 #[test]
478 fn instance_defaults_forwarded_into_hooks() {
479 let doc = compile(
480 r#"
481 component Item inherits Rectangle {
482 in property <color> tint: blue;
483 background: tint;
484 }
485 export component Win inherits Window {
486 width: 100px; height: 100px;
487 plain := Item { }
488 for _idx in 2: Item { }
489 }
490 "#,
491 );
492 let win = component(&doc, "Win");
493
494 let assert_background_preserved = |element: &ElementRc, what: &str| {
495 let borrowed = element.borrow();
496 let binding_expression = borrowed
497 .binding_cell_including_synthetic("background")
498 .unwrap_or_else(|| panic!("{what}: background must be bound"));
499 let expression = binding_expression.borrow().expression.clone();
500 let Expression::DebugHook { expression: inner, id, synthetic } = expression else {
501 panic!("{what}: background must be a DebugHook, got {expression:?}");
502 };
503 assert!(synthetic, "{what}: inherited hook must remain synthetic");
504 let mut references_tint = false;
505 inner.visit_recursive(&mut |expression| {
506 if let Expression::PropertyReference(named_reference) = expression
507 && named_reference.name().ends_with("tint")
508 {
509 references_tint = true;
510 }
511 });
512 assert!(references_tint, "{what}: background must still reference tint, got {inner:?}");
513 assert_eq!(
514 property_id(
515 borrowed.debug[0].element_hash,
516 &smol_str::SmolStr::new_static("background")
517 ),
518 id,
519 "{what}: hook id must use the instance element hash"
520 );
521 assert!(
522 matches!(borrowed.base_type, crate::langtype::ElementType::Component(_)),
523 "{what}: component boundary must remain"
524 );
525 };
526
527 let plain = child(&win.root_element, "plain");
528 assert_background_preserved(&plain, "plain instance");
529
530 let repeated = win
531 .root_element
532 .borrow()
533 .children
534 .iter()
535 .find(|c| c.borrow().repeated.is_some())
536 .expect("repeated element")
537 .clone();
538 let repeated_base = repeated.borrow().base_type.as_component().clone();
539 let mut found = None;
540 object_tree::recurse_elem(&repeated_base.root_element, &(), &mut |elem, &()| {
541 if elem.borrow().binding_cell_including_synthetic("background").is_some() {
542 found = Some(elem.clone());
543 }
544 });
545 let repeated_item = found.expect("repeated Item element with background binding");
546 assert_background_preserved(&repeated_item, "repeated instance");
547 }
548
549 #[test]
550 fn reuses_forwarded_references_for_hooks_and_states() {
551 let doc = compile(
552 r#"
553 component Item inherits Rectangle {
554 in-out property <int> property-value: inner.width / 1px;
555 in-out property <int> function-value: inner.compute-value(3);
556 in-out property <int> callback-value: inner.compute-callback(5);
557 inner := Rectangle {
558 width: 10px;
559 pure function compute-value(value: int) -> int { value + 10 }
560 pure callback compute-callback(int) -> int;
561 compute-callback(value) => value * 2;
562 }
563 }
564 export component Win inherits Window {
565 in property <bool> active;
566 first := Item { }
567 second := Item { }
568 states [
569 active when root.active: {
570 first.property-value: 42;
571 first.function-value: 43;
572 first.callback-value: 44;
573 }
574 ]
575 }
576 "#,
577 );
578 let item = component(&doc, "Item");
579 let win = component(&doc, "Win");
580 let first = child(&win.root_element, "first");
581 let second = child(&win.root_element, "second");
582
583 let declarations = item
584 .root_element
585 .borrow()
586 .property_declarations
587 .keys()
588 .filter(|name| name.starts_with("forward_reference_"))
589 .cloned()
590 .collect::<std::collections::HashSet<_>>();
591 assert_eq!(declarations.len(), 3);
592
593 let forwarded_references = |element: &ElementRc| {
594 let element = element.borrow();
595 let mut references = std::collections::HashSet::new();
596 for property_name in ["property-value", "function-value", "callback-value"] {
597 let binding = element
598 .binding_cell_including_synthetic(property_name)
599 .unwrap_or_else(|| panic!("{property_name} must be hooked"));
600 binding.borrow().expression.visit_recursive(&mut |expression| match expression {
601 Expression::PropertyReference(named_reference)
602 | Expression::FunctionCall {
603 function:
604 crate::expression_tree::Callable::Callback(named_reference)
605 | crate::expression_tree::Callable::Function(named_reference),
606 ..
607 } if named_reference.name().starts_with("forward_reference_") => {
608 references.insert(named_reference.name().clone());
609 }
610 _ => {}
611 });
612 }
613 references
614 };
615
616 let first_references = forwarded_references(&first);
617 let second_references = forwarded_references(&second);
618 assert!(!first_references.is_empty());
619 assert!(!second_references.is_empty());
620 assert!(first_references.is_subset(&declarations));
621 assert!(second_references.is_subset(&declarations));
622 assert!(matches!(first.borrow().base_type, crate::langtype::ElementType::Component(_)));
623 assert!(matches!(second.borrow().base_type, crate::langtype::ElementType::Component(_)));
624 }
625
626 #[test]
627 fn inherited_two_way_binding_has_no_hook() {
628 let doc = compile(
629 r#"
630 component Item inherits Rectangle {
631 in-out property <length> linked <=> target;
632 in-out property <length> target: 42px;
633 }
634 export component Win inherits Window {
635 item := Item { }
636 }
637 "#,
638 );
639 let win = component(&doc, "Win");
640 let item = child(&win.root_element, "item");
641
642 assert!(item.borrow().binding_cell_including_synthetic("linked").is_none());
643 assert!(item.borrow().binding_cell_including_synthetic("target").is_none());
644 assert!(matches!(item.borrow().base_type, crate::langtype::ElementType::Component(_)));
645 }
646
647 #[test]
650 fn merge_with_synthetic_hook_rules() {
651 use crate::expression_tree::BindingExpression;
652
653 let synthetic_hook = || -> BindingExpression {
654 Expression::DebugHook {
655 expression: Box::new(Expression::NumberLiteral(0., Default::default())),
656 id: "?42-prop".into(),
657 synthetic: true,
658 }
659 .into()
660 };
661 let real_binding = |value: f64| -> BindingExpression {
662 let mut binding: BindingExpression =
663 Expression::NumberLiteral(value, Default::default()).into();
664 binding.priority = 3;
665 binding
666 };
667
668 let mut binding = synthetic_hook();
670 assert!(binding.merge_with(&real_binding(7.)), "the other expression must be taken");
671 match &binding.expression {
672 Expression::DebugHook { expression, id, synthetic } => {
673 assert!(!synthetic, "upgraded hook must no longer be synthetic");
674 assert_eq!(id, "?42-prop", "the hook id must survive the merge");
675 assert!(
676 matches!(**expression, Expression::NumberLiteral(v, _) if v == 7.),
677 "the definition's expression must be taken"
678 );
679 }
680 other => panic!("expected an upgraded DebugHook, got {other:?}"),
681 }
682 assert_eq!(binding.priority, 3, "the other side's priority must be taken");
683
684 let mut binding = synthetic_hook();
686 assert!(!binding.merge_with(&synthetic_hook()));
687 assert!(binding.expression.is_synthetic_debug_hook());
688
689 let mut binding = synthetic_hook();
692 let mut two_way: BindingExpression = Expression::Invalid.into();
693 two_way.two_way_bindings.push(crate::expression_tree::TwoWayBinding::ModelData {
694 repeated_element: std::rc::Weak::default(),
695 field_access: Default::default(),
696 });
697 assert!(binding.merge_with(&two_way));
698 assert!(matches!(binding.expression, Expression::Invalid));
699 assert_eq!(binding.two_way_bindings.len(), 1);
700
701 let mut binding: BindingExpression = Expression::DebugHook {
703 expression: Box::new(Expression::NumberLiteral(1., Default::default())),
704 id: "?42-prop".into(),
705 synthetic: false,
706 }
707 .into();
708 assert!(!binding.merge_with(&real_binding(9.)));
709 assert!(matches!(binding.value_expression(), Expression::NumberLiteral(v, _) if *v == 1.));
710 }
711
712 #[test]
717 fn transform_rotation_hook_is_reified() {
718 let doc = compile(
719 r#"
720 export component Foo inherits Window {
721 rect := Rectangle { }
722 }
723 "#,
724 );
725 let foo = component(&doc, "Foo");
726 let rect = child(&foo.root_element, "rect");
727 let rect_hash = rect.borrow().debug.first().unwrap().element_hash;
728 let rotation_hook_id =
729 property_id(rect_hash, &smol_str::SmolStr::new_static("transform-rotation"));
730
731 let transform_element = child(&foo.root_element, "rect-Transform");
733
734 let binding_holder = transform_element.borrow();
738 let binding_expression = binding_holder
739 .binding_cell_including_synthetic("transform-rotation")
740 .expect("the Transform element must bind transform-rotation");
741 match &binding_expression.borrow().expression {
742 Expression::DebugHook { id, synthetic, expression } => {
743 assert_eq!(id, &rotation_hook_id, "hook id must be derived from rect's hash");
744 assert!(!synthetic, "the injected rotation hook must be non-synthetic");
745 assert!(
746 matches!(**expression, Expression::NumberLiteral(v, _) if v == 0.),
747 "the rotation hook must wrap the 0deg default"
748 );
749 }
750 other => panic!("transform-rotation must be a DebugHook, got {other:?}"),
751 }
752 }
753
754 #[test]
757 fn geometry_defaults_still_set_with_debug_hooks() {
758 let doc = compile(
759 r#"
760 export component Foo inherits Window {
761 img := Image { source: @image-url("nonexistent.png"); }
762 }
763 "#,
764 );
765 let foo = component(&doc, "Foo");
766 let img = child(&foo.root_element, "img");
767 let img_hash = img.borrow().debug.first().unwrap().element_hash;
768
769 let find_hook_by_id = |wanted_id: &smol_str::SmolStr| -> Option<Expression> {
773 let mut found = None;
774 object_tree::recurse_elem(&foo.root_element, &(), &mut |elem, &()| {
775 for (_, binding_expression) in elem.borrow().bindings_including_synthetic() {
776 if let Expression::DebugHook { id, .. } =
777 &binding_expression.borrow().expression
778 && id == wanted_id
779 {
780 found = Some(binding_expression.borrow().expression.clone());
781 }
782 }
783 });
784 found
785 };
786
787 for property in ["x", "y", "width", "height"] {
788 let hook_id = property_id(img_hash, &smol_str::SmolStr::new_static(property));
794 let expression = find_hook_by_id(&hook_id)
795 .unwrap_or_else(|| panic!("a debug hook for img.{property} must survive"));
796 assert!(
797 matches!(expression, Expression::DebugHook { synthetic: false, .. }),
798 "img.{property} hook should not be synthetic after default_geometry, got {expression:?}"
799 );
800 }
801 }
802}