Skip to main content

i_slint_compiler/passes/
const_propagation.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Try to simplify property bindings by propagating constant expressions
5
6use 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    // The binding analysis classifies conversions such as float to string as non-constant
27    // because their result depends on the locale's decimal separator. When the
28    // simplification folded the conversion away, the binding is constant after all:
29    // promote it back.
30    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
46/// Returns false if the expression still contains a reference to an element
47fn 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                // Inline the constant value
63                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                // TODO: take care of * and / when both numbers have units
100                ('=' | '!', 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                // TODO: more types and more comparison operators
110                ('&', 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        // disable this simplification for store local variable, as "let" is not an expression in rust
230        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
266/// Will extract the property binding from the given named reference
267/// and propagate constant expression within it. If that's possible,
268/// return the new expression. Results are cached per NamedReference.
269fn 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
283/// Extract just one field from a constant struct property, cloning only that
284/// field instead of the entire struct expression.
285fn extract_struct_field_from_constant(
286    nr: &NamedReference,
287    field_name: &str,
288    ga: &GlobalAnalysis,
289    cache: &mut ConstPropCache,
290) -> Option<Expression> {
291    // Populate the cache via the canonical path (result itself is discarded)
292    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    // find the binding.
306    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                // TODO: In practice, we should still find out what the real binding is
312                // and solve that.
313                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        // There is no binding for this property, return the default value
329        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            // Only fold when the string can't contain the decimal separator of any locale,
400            // so that parsing gives the same result regardless of the locale.
401            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        // We have a code block because the first entry stores the value of `input` in a local variable
468        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    // Conversions whose result contains no decimal separator are folded and stay constant
511    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    // Locale-dependent conversions are not folded and their bindings are no longer constant,
527    // so that they are re-evaluated when the locale changes at runtime
528    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}