1use std::collections::HashMap;
7
8use super::GlobalAnalysis;
9use crate::expression_tree::*;
10use crate::langtype::{BuiltinStruct, ElementType, StructName, Type};
11use crate::namedreference::NamedReference;
12use crate::object_tree::*;
13use smol_str::format_smolstr;
14
15type ConstPropCache = HashMap<NamedReference, Option<Expression>>;
16
17pub fn const_propagation(component: &Component, global_analysis: &GlobalAnalysis) {
18 let mut cache = ConstPropCache::new();
19 visit_all_expressions(component, |expr, ty| {
20 if matches!(ty(), Type::Callback { .. }) {
21 return;
22 }
23 simplify_expression(expr, global_analysis, &mut cache);
24 });
25
26 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
31 for binding in elem.borrow().bindings.values() {
32 let Ok(mut binding) = binding.try_borrow_mut() else { continue };
33 let Some(analysis) = binding.analysis.as_ref() else { continue };
34 if analysis.is_const || matches!(binding.expression, Expression::Invalid) {
35 continue;
36 }
37 if binding.expression.is_constant(Some(global_analysis))
38 && binding.two_way_bindings.iter().all(|tw| tw.is_constant())
39 {
40 binding.analysis.as_mut().unwrap().is_const = true;
41 }
42 }
43 });
44}
45
46fn simplify_expression(
48 expr: &mut Expression,
49 ga: &GlobalAnalysis,
50 cache: &mut ConstPropCache,
51) -> bool {
52 match expr {
53 Expression::PropertyReference(nr) => {
54 if nr.is_constant()
55 && !match nr.ty() {
56 Type::Struct(s) => {
57 matches!(s.name, StructName::Builtin(BuiltinStruct::StateInfo))
58 }
59 _ => false,
60 }
61 {
62 if let Some(result) = extract_constant_property_reference(nr, ga, cache) {
64 *expr = result;
65 return true;
66 }
67 }
68 false
69 }
70 Expression::BinaryExpression { lhs, op, rhs } => {
71 let mut can_inline = simplify_expression(lhs, ga, cache);
72 can_inline &= simplify_expression(rhs, ga, cache);
73
74 let new = match (*op, &mut **lhs, &mut **rhs) {
75 ('+', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
76 Some(Expression::StringLiteral(format_smolstr!("{}{}", a, b)))
77 }
78 ('+', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
79 if un1 == un2 =>
80 {
81 Some(Expression::NumberLiteral(*a + *b, *un1))
82 }
83 ('-', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
84 if un1 == un2 =>
85 {
86 Some(Expression::NumberLiteral(*a - *b, *un1))
87 }
88 ('*', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
89 if *un1 == Unit::None || *un2 == Unit::None =>
90 {
91 let preserved_unit = if *un1 == Unit::None { *un2 } else { *un1 };
92 Some(Expression::NumberLiteral(*a * *b, preserved_unit))
93 }
94 (
95 '/',
96 Expression::NumberLiteral(a, un1),
97 Expression::NumberLiteral(b, Unit::None),
98 ) => Some(Expression::NumberLiteral(*a / *b, *un1)),
99 ('=' | '!', Expression::NumberLiteral(a, _), Expression::NumberLiteral(b, _)) => {
101 Some(Expression::BoolLiteral((a == b) == (*op == '=')))
102 }
103 ('=' | '!', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
104 Some(Expression::BoolLiteral((a == b) == (*op == '=')))
105 }
106 ('=' | '!', Expression::EnumerationValue(a), Expression::EnumerationValue(b)) => {
107 Some(Expression::BoolLiteral((a == b) == (*op == '=')))
108 }
109 ('&', Expression::BoolLiteral(false), _) => {
111 can_inline = true;
112 Some(Expression::BoolLiteral(false))
113 }
114 ('&', _, Expression::BoolLiteral(false)) => {
115 can_inline = true;
116 Some(Expression::BoolLiteral(false))
117 }
118 ('&', Expression::BoolLiteral(true), e) => Some(std::mem::take(e)),
119 ('&', e, Expression::BoolLiteral(true)) => Some(std::mem::take(e)),
120 ('|', Expression::BoolLiteral(true), _) => {
121 can_inline = true;
122 Some(Expression::BoolLiteral(true))
123 }
124 ('|', _, Expression::BoolLiteral(true)) => {
125 can_inline = true;
126 Some(Expression::BoolLiteral(true))
127 }
128 ('|', Expression::BoolLiteral(false), e) => Some(std::mem::take(e)),
129 ('|', e, Expression::BoolLiteral(false)) => Some(std::mem::take(e)),
130 ('>', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
131 if un1 == un2 =>
132 {
133 Some(Expression::BoolLiteral(*a > *b))
134 }
135 ('<', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
136 if un1 == un2 =>
137 {
138 Some(Expression::BoolLiteral(*a < *b))
139 }
140 _ => None,
141 };
142 if let Some(new) = new {
143 *expr = new;
144 }
145 can_inline
146 }
147 Expression::UnaryOp { sub, op } => {
148 let can_inline = simplify_expression(sub, ga, cache);
149 let new = match (*op, &mut **sub) {
150 ('!', Expression::BoolLiteral(b)) => Some(Expression::BoolLiteral(!*b)),
151 ('-', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(-*n, *u)),
152 ('+', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(*n, *u)),
153 _ => None,
154 };
155 if let Some(new) = new {
156 *expr = new;
157 }
158 can_inline
159 }
160 Expression::StructFieldAccess { base, name } => {
161 if let Expression::PropertyReference(nr) = &**base
162 && nr.is_constant()
163 && let Some(field_expr) = extract_struct_field_from_constant(nr, name, ga, cache)
164 {
165 *expr = field_expr;
166 return simplify_expression(expr, ga, cache);
167 }
168 let r = simplify_expression(base, ga, cache);
169 if let Expression::Struct { values, .. } = &mut **base
170 && let Some(e) = values.remove(name)
171 {
172 *expr = e;
173 return simplify_expression(expr, ga, cache);
174 }
175 r
176 }
177 Expression::Cast { from, to } => {
178 let can_inline = simplify_expression(from, ga, cache);
179 let new = if from.ty() == *to {
180 Some(std::mem::take(&mut **from))
181 } else {
182 match (&**from, to) {
183 (Expression::NumberLiteral(x, Unit::None), Type::String) => {
184 locale_independent_number_to_string(*x).map(Expression::StringLiteral)
185 }
186 (Expression::Struct { values, .. }, Type::Struct(ty)) => {
187 Some(Expression::Struct { ty: ty.clone(), values: values.clone() })
188 }
189 _ => None,
190 }
191 };
192 if let Some(new) = new {
193 *expr = new;
194 }
195 can_inline
196 }
197 Expression::MinMax { op, lhs, rhs, ty: _ } => {
198 let can_inline =
199 simplify_expression(lhs, ga, cache) & simplify_expression(rhs, ga, cache);
200 if let (Expression::NumberLiteral(lhs, u), Expression::NumberLiteral(rhs, _)) =
201 (&**lhs, &**rhs)
202 {
203 let v = match op {
204 MinMaxOp::Min => lhs.min(*rhs),
205 MinMaxOp::Max => lhs.max(*rhs),
206 };
207 *expr = Expression::NumberLiteral(v, *u);
208 }
209 can_inline
210 }
211 Expression::Condition { condition, true_expr, false_expr } => {
212 let mut can_inline = simplify_expression(condition, ga, cache);
213 can_inline &= match &**condition {
214 Expression::BoolLiteral(true) => {
215 *expr = *true_expr.clone();
216 simplify_expression(expr, ga, cache)
217 }
218 Expression::BoolLiteral(false) => {
219 *expr = *false_expr.clone();
220 simplify_expression(expr, ga, cache)
221 }
222 _ => {
223 simplify_expression(true_expr, ga, cache)
224 & simplify_expression(false_expr, ga, cache)
225 }
226 };
227 can_inline
228 }
229 Expression::CodeBlock(stmts)
231 if stmts.len() == 1 && !matches!(stmts[0], Expression::StoreLocalVariable { .. }) =>
232 {
233 *expr = stmts[0].clone();
234 simplify_expression(expr, ga, cache)
235 }
236 Expression::FunctionCall { function, arguments, .. } => {
237 let mut args_can_inline = true;
238 for arg in arguments.iter_mut() {
239 args_can_inline &= simplify_expression(arg, ga, cache);
240 }
241 if args_can_inline
242 && let Some(inlined) = try_inline_function(function, arguments, ga, cache)
243 {
244 *expr = inlined;
245 return true;
246 }
247 false
248 }
249 Expression::ElementReference { .. } => false,
250 Expression::LayoutCacheAccess { .. } => false,
251 Expression::OrganizeGridLayout { .. } => false,
252 Expression::SolveBoxLayout { .. } => false,
253 Expression::SolveGridLayout { .. } => false,
254 Expression::SolveFlexboxLayout { .. } => false,
255 Expression::ComputeBoxLayoutInfo { .. } => false,
256 Expression::ComputeGridLayoutInfo { .. } => false,
257 Expression::ComputeFlexboxLayoutInfo { .. } => false,
258 _ => {
259 let mut result = true;
260 expr.visit_mut(|expr| result &= simplify_expression(expr, ga, cache));
261 result
262 }
263 }
264}
265
266fn extract_constant_property_reference(
270 nr: &NamedReference,
271 ga: &GlobalAnalysis,
272 cache: &mut ConstPropCache,
273) -> Option<Expression> {
274 debug_assert!(nr.is_constant());
275 if let Some(cached) = cache.get(nr) {
276 return cached.clone();
277 }
278 let result = extract_constant_property_reference_impl(nr, ga, cache);
279 cache.insert(nr.clone(), result.clone());
280 result
281}
282
283fn extract_struct_field_from_constant(
286 nr: &NamedReference,
287 field_name: &str,
288 ga: &GlobalAnalysis,
289 cache: &mut ConstPropCache,
290) -> Option<Expression> {
291 let _ = extract_constant_property_reference(nr, ga, cache);
293 if let Some(Some(Expression::Struct { values, .. })) = cache.get(nr) {
294 values.get(field_name).cloned()
295 } else {
296 None
297 }
298}
299
300fn extract_constant_property_reference_impl(
301 nr: &NamedReference,
302 ga: &GlobalAnalysis,
303 cache: &mut ConstPropCache,
304) -> Option<Expression> {
305 let mut element = nr.element();
307 let mut expression = loop {
308 if let Some(binding) = element.borrow().bindings.get(nr.name()) {
309 let binding = binding.borrow();
310 if !binding.two_way_bindings.is_empty() {
311 return None;
314 }
315 if !matches!(binding.expression, Expression::Invalid) {
316 break binding.expression.clone();
317 }
318 };
319 if let Some(decl) = element.clone().borrow().property_declarations.get(nr.name()) {
320 if let Some(alias) = &decl.is_alias {
321 return extract_constant_property_reference(alias, ga, cache);
322 }
323 } else if let ElementType::Component(c) = &element.clone().borrow().base_type {
324 element = c.root_element.clone();
325 continue;
326 }
327
328 let ty = nr.ty();
330 debug_assert!(!matches!(ty, Type::Invalid));
331 return Some(Expression::default_value_for_type(&ty));
332 };
333 if !(simplify_expression(&mut expression, ga, cache)) {
334 return None;
335 }
336 Some(expression)
337}
338
339fn try_inline_function(
340 function: &Callable,
341 arguments: &[Expression],
342 ga: &GlobalAnalysis,
343 cache: &mut ConstPropCache,
344) -> Option<Expression> {
345 let function = match function {
346 Callable::Function(function) => function,
347 Callable::Builtin(b) => return try_inline_builtin_function(b, arguments, ga),
348 _ => return None,
349 };
350 if !function.is_constant() {
351 return None;
352 }
353 let mut body = extract_constant_property_reference(function, ga, cache)?;
354
355 fn substitute_arguments_recursive(e: &mut Expression, arguments: &[Expression]) {
356 if let Expression::FunctionParameterReference { index, ty } = e {
357 let e_new = arguments.get(*index).expect("reference to invalid arg").clone();
358 debug_assert_eq!(e_new.ty(), *ty);
359 *e = e_new;
360 } else {
361 e.visit_mut(|e| substitute_arguments_recursive(e, arguments));
362 }
363 }
364 substitute_arguments_recursive(&mut body, arguments);
365
366 if simplify_expression(&mut body, ga, cache) { Some(body) } else { None }
367}
368
369fn try_inline_builtin_function(
370 b: &BuiltinFunction,
371 args: &[Expression],
372 ga: &GlobalAnalysis,
373) -> Option<Expression> {
374 let a = |idx: usize| -> Option<f64> {
375 match args.get(idx)? {
376 Expression::NumberLiteral(n, Unit::None) => Some(*n),
377 _ => None,
378 }
379 };
380 let num = |n: f64| Some(Expression::NumberLiteral(n, Unit::None));
381
382 match b {
383 BuiltinFunction::GetWindowScaleFactor => {
384 ga.const_scale_factor.map(|factor| Expression::NumberLiteral(factor as _, Unit::None))
385 }
386 BuiltinFunction::GetWindowDefaultFontSize => match ga.default_font_size {
387 crate::passes::binding_analysis::DefaultFontSize::LogicalValue(val) => {
388 Some(Expression::NumberLiteral(val as _, Unit::Px))
389 }
390 _ => None,
391 },
392 BuiltinFunction::Mod => num(a(0)?.rem_euclid(a(1)?)),
393 BuiltinFunction::Round => num(a(0)?.round()),
394 BuiltinFunction::Ceil => num(a(0)?.ceil()),
395 BuiltinFunction::Floor => num(a(0)?.floor()),
396 BuiltinFunction::Abs => num(a(0)?.abs()),
397 BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => {
398 let Some(Expression::StringLiteral(s)) = args.first() else { return None };
399 if !s.chars().all(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | 'e' | 'E')) {
402 return None;
403 }
404 let value = s.parse::<f32>().ok();
405 Some(match b {
406 BuiltinFunction::StringToFloat => {
407 Expression::NumberLiteral(value.unwrap_or(0.) as f64, Unit::None)
408 }
409 _ => Expression::BoolLiteral(value.is_some()),
410 })
411 }
412 _ => None,
413 }
414}
415
416#[test]
417fn test() {
418 let mut compiler_config =
419 crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
420 compiler_config.style = Some("fluent".into());
421 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
422 let doc_node = crate::parser::parse(
423 r#"
424/* ... */
425struct Hello { s: string, v: float }
426enum Enum { aa, bb, cc }
427global G {
428 pure function complicated(a: float ) -> bool { if a > 5 { return true; }; if a < 1 { return true; }; uncomplicated() }
429 pure function uncomplicated( ) -> bool { false }
430 out property <float> p : 3 * 2 + 15 ;
431 property <string> q: "foo " + 42;
432 out property <float> w : -p / 2;
433 out property <Hello> out: { s: q, v: complicated(w + 15) ? -123 : p };
434
435 in-out property <Enum> e: Enum.bb;
436}
437export component Foo {
438 in property <int> input;
439 out property<float> out1: G.w;
440 out property<float> out2: G.out.v;
441 out property<bool> out3: false ? input == 12 : input > 0 ? input == 11 : G.e == Enum.bb;
442}
443"#
444 .into(),
445 Some(std::path::Path::new("HELLO")),
446 &mut test_diags,
447 );
448 let (doc, diag, _) =
449 spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
450 assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
451
452 let expected_p = 3.0 * 2.0 + 15.0;
453 let expected_w = -expected_p / 2.0;
454 let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
455 let out1_binding = bindings.get("out1").unwrap().borrow().expression.clone();
456 match &out1_binding {
457 Expression::NumberLiteral(n, _) => assert_eq!(*n, expected_w),
458 _ => panic!("not number {out1_binding:?}"),
459 }
460 let out2_binding = bindings.get("out2").unwrap().borrow().expression.clone();
461 match &out2_binding {
462 Expression::NumberLiteral(n, _) => assert_eq!(*n, expected_p),
463 _ => panic!("not number {out2_binding:?}"),
464 }
465 let out3_binding = bindings.get("out3").unwrap().borrow().expression.clone();
466 match &out3_binding {
467 Expression::CodeBlock(stmts) => match &stmts[1] {
469 Expression::Condition { condition: _, true_expr: _, false_expr } => match &**false_expr
470 {
471 Expression::BoolLiteral(b) => assert!(*b),
472 _ => panic!("false_expr not optimized in : {out3_binding:?}"),
473 },
474 _ => panic!("not condition: {out3_binding:?}"),
475 },
476 _ => panic!("not code block: {out3_binding:?}"),
477 };
478}
479
480#[test]
481fn test_locale_dependent_string_conversion() {
482 let mut compiler_config =
483 crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
484 compiler_config.style = Some("fluent".into());
485 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
486 let doc_node = crate::parser::parse(
487 r#"
488export component Foo {
489 out property <string> int-str: "n=" + 42;
490 out property <string> calc-str: "n=" + (6 * 7);
491 out property <string> float-str: "n=" + 4.5;
492 out property <float> int-float: "42".to-float();
493 out property <bool> int-is-float: "42".is-float();
494 out property <float> frac-float: "4,2".to-float();
495}
496"#
497 .into(),
498 Some(std::path::Path::new("HELLO")),
499 &mut test_diags,
500 );
501 let (doc, diag, _) =
502 spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
503 assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
504
505 let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
506 let binding = |name: &str| bindings.get(name).unwrap().borrow().clone();
507 let is_const =
508 |name: &str| bindings.get(name).unwrap().borrow().analysis.as_ref().unwrap().is_const;
509
510 assert!(
512 matches!(&binding("int-str").expression, Expression::StringLiteral(s) if s == "n=42"),
513 "{:?}",
514 binding("int-str").expression
515 );
516 assert!(is_const("int-str"));
517 assert!(matches!(&binding("calc-str").expression, Expression::StringLiteral(s) if s == "n=42"));
518 assert!(is_const("calc-str"));
519 assert!(
520 matches!(&binding("int-float").expression, Expression::NumberLiteral(n, _) if *n == 42.)
521 );
522 assert!(is_const("int-float"));
523 assert!(matches!(&binding("int-is-float").expression, Expression::BoolLiteral(true)));
524 assert!(is_const("int-is-float"));
525
526 assert!(
529 !matches!(&binding("float-str").expression, Expression::StringLiteral(_)),
530 "{:?}",
531 binding("float-str").expression
532 );
533 assert!(!is_const("float-str"));
534 assert!(matches!(&binding("frac-float").expression, Expression::FunctionCall { .. }));
535 assert!(!is_const("frac-float"));
536}
537
538#[test]
539fn test_propagate_font_size() {
540 struct Case {
541 default_font_size: &'static str,
542 another_window: &'static str,
543 check_expression: fn(&Expression),
544 }
545
546 #[track_caller]
547 fn assert_expr_is_mul(e: &Expression, l: f64, r: f64) {
548 assert!(
549 matches!(e, Expression::Cast { from, .. }
550 if matches!(from.as_ref(), Expression::BinaryExpression { lhs, rhs, op: '*'}
551 if matches!((lhs.as_ref(), rhs.as_ref()), (Expression::NumberLiteral(lhs, _), Expression::NumberLiteral(rhs, _)) if *lhs == l && *rhs == r ))),
552 "Expression {e:?} is not a {l} * {r} expected"
553 );
554 }
555
556 for Case { default_font_size, another_window, check_expression } in [
557 Case {
558 default_font_size: "default-font-size: 12px;",
559 another_window: "",
560 check_expression: |e| assert_expr_is_mul(e, 5.0, 12.0),
561 },
562 Case {
563 default_font_size: "default-font-size: some-value;",
564 another_window: "",
565 check_expression: |e| {
566 assert!(
567 !e.is_constant(None),
568 "{e:?} should not be constant since some-value can vary at runtime"
569 );
570 },
571 },
572 Case {
573 default_font_size: "default-font-size: 25px;",
574 another_window: "export component AnotherWindow inherits Window { default-font-size: 8px; }",
575 check_expression: |e| {
576 assert!(
577 e.is_constant(None) && !matches!(e, Expression::NumberLiteral(_, _)),
578 "{e:?} should be constant but not known at compile time since there are two windows"
579 );
580 },
581 },
582 Case {
583 default_font_size: "default-font-size: 25px;",
584 another_window: "export component AnotherWindow inherits Window { }",
585 check_expression: |e| {
586 assert!(
587 !e.is_constant(None),
588 "should not be const since at least one window has it unset"
589 );
590 },
591 },
592 Case {
593 default_font_size: "default-font-size: 20px;",
594 another_window: "export component AnotherWindow inherits Window { default-font-size: 20px; }",
595 check_expression: |e| assert_expr_is_mul(e, 5.0, 20.0),
596 },
597 Case {
598 default_font_size: "default-font-size: 20px;",
599 another_window: "export component AnotherWindow inherits Window { in property <float> f: 1; default-font-size: 20px*f; }",
600 check_expression: |e| {
601 assert!(
602 !e.is_constant(None),
603 "{e:?} should not be constant since 'f' can vary at runtime"
604 );
605 },
606 },
607 ] {
608 let source = format!(
609 r#"
610component SomeComponent {{
611 in-out property <length> rem-prop: 5rem;
612}}
613
614{another_window}
615
616export component Foo inherits Window {{
617 in property <length> some-value: 45px;
618 {default_font_size}
619 sc1 := SomeComponent {{}}
620 sc2 := SomeComponent {{}}
621
622 out property <length> test: sc1.rem-prop;
623}}
624"#
625 );
626
627 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
628
629 let doc_node = crate::parser::parse(
630 source.clone(),
631 Some(std::path::Path::new("HELLO")),
632 &mut test_diags,
633 );
634 let mut compiler_config =
635 crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
636 compiler_config.style = Some("fluent".into());
637 let (doc, diag, _) =
638 spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
639 assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
640
641 let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
642 let out1_binding = bindings.get("test").unwrap().borrow().expression.clone();
643 check_expression(&out1_binding);
644 }
645}
646
647#[test]
648fn test_const_scale_factor() {
649 let source = r#"
650export component Foo inherits Window {
651 out property <length> test: 10phx;
652}"#;
653
654 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
655 let doc_node = crate::parser::parse(
656 source.to_string(),
657 Some(std::path::Path::new("HELLO")),
658 &mut test_diags,
659 );
660 let mut compiler_config =
661 crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
662 compiler_config.style = Some("fluent".into());
663 compiler_config.const_scale_factor = Some(2.);
664 let (doc, diag, _) =
665 spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
666 assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
667
668 let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
669 let mut test_binding = bindings.get("test").unwrap().borrow().expression.clone();
670 if let Expression::Cast { from, to: _ } = test_binding {
671 test_binding = *from;
672 }
673 assert!(
674 matches!(test_binding, Expression::NumberLiteral(val, _) if val == 5.0),
675 "Expression should be 5.0: {test_binding:?}"
676 );
677}