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
17/// Fold constants in an expression that stands on its own, outside of a component.
18///
19/// This is used for expressions that cannot reference any properties or elements,
20/// such as the default values of struct fields.
21pub(crate) fn fold_const_expression(expr: &mut Expression) {
22    simplify_expression(expr, &GlobalAnalysis::default(), &mut ConstPropCache::default());
23}
24
25pub fn const_propagation(component: &Component, global_analysis: &GlobalAnalysis) {
26    let mut cache = ConstPropCache::new();
27    visit_all_expressions(component, |expr, _ty| {
28        simplify_expression(expr, global_analysis, &mut cache);
29    });
30
31    // The binding analysis classifies conversions such as float to string as non-constant
32    // because their result depends on the locale's decimal separator. When the
33    // simplification folded the conversion away, the binding is constant after all:
34    // promote it back.
35    recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
36        for (_, binding) in elem.borrow().real_bindings() {
37            let Ok(mut binding) = binding.try_borrow_mut() else { continue };
38            let Some(analysis) = binding.analysis.as_ref() else { continue };
39            if analysis.is_const || matches!(binding.expression, Expression::Invalid) {
40                continue;
41            }
42            if binding.expression.is_constant(Some(global_analysis))
43                && binding.two_way_bindings.iter().all(|tw| tw.is_constant())
44            {
45                binding.analysis.as_mut().unwrap().is_const = true;
46            }
47        }
48    });
49}
50
51/// Returns false if the expression still contains a reference to an element
52///
53/// The body of every non-trivial match arm lives in its own `#[inline(never)]`
54/// helper function: this function recurses for nested expressions, and with all
55/// arm bodies inlined, its stack frame in unoptimized builds becomes so large
56/// that deeply nested expressions overflow the stack.
57fn simplify_expression(
58    expr: &mut Expression,
59    ga: &GlobalAnalysis,
60    cache: &mut ConstPropCache,
61) -> bool {
62    match expr {
63        Expression::PropertyReference(..) => simplify_property_reference(expr, ga, cache),
64        Expression::BinaryExpression { .. } => simplify_binary_expression(expr, ga, cache),
65        Expression::UnaryOp { .. } => simplify_unary_op(expr, ga, cache),
66        Expression::StructFieldAccess { .. } => simplify_struct_field_access(expr, ga, cache),
67        Expression::Cast { .. } => simplify_cast(expr, ga, cache),
68        Expression::MinMax { .. } => simplify_min_max(expr, ga, cache),
69        Expression::Condition { .. } => simplify_condition(expr, ga, cache),
70        // disable this simplification for store local variable, as "let" is not an expression in rust
71        Expression::CodeBlock(stmts)
72            if stmts.len() == 1 && !matches!(stmts[0], Expression::StoreLocalVariable { .. }) =>
73        {
74            simplify_single_statement_code_block(expr, ga, cache)
75        }
76        Expression::FunctionCall { .. } => simplify_function_call(expr, ga, cache),
77        Expression::ElementReference { .. } => false,
78        Expression::LayoutCacheAccess { .. } => false,
79        Expression::OrganizeGridLayout { .. } => false,
80        Expression::SolveBoxLayout { .. } => false,
81        Expression::SolveGridLayout { .. } => false,
82        Expression::SolveFlexboxLayout { .. } => false,
83        Expression::ComputeBoxLayoutInfo { .. } => false,
84        Expression::ComputeGridLayoutInfo { .. } => false,
85        Expression::ComputeFlexboxLayoutInfo { .. } => false,
86        _ => {
87            let mut result = true;
88            expr.visit_mut(|expr| result &= simplify_expression(expr, ga, cache));
89            result
90        }
91    }
92}
93
94#[inline(never)]
95fn simplify_property_reference(
96    expr: &mut Expression,
97    ga: &GlobalAnalysis,
98    cache: &mut ConstPropCache,
99) -> bool {
100    let Expression::PropertyReference(nr) = expr else { unreachable!() };
101    if nr.is_constant()
102        && !match nr.ty() {
103            Type::Struct(s) => {
104                matches!(s.name, StructName::Builtin(BuiltinStruct::StateInfo))
105            }
106            _ => false,
107        }
108    {
109        // Inline the constant value
110        if let Some(result) = extract_constant_property_reference(nr, ga, cache) {
111            *expr = result;
112            return true;
113        }
114    }
115    false
116}
117
118#[inline(never)]
119fn simplify_binary_expression(
120    expr: &mut Expression,
121    ga: &GlobalAnalysis,
122    cache: &mut ConstPropCache,
123) -> bool {
124    let Expression::BinaryExpression { lhs, op, rhs, .. } = expr else { unreachable!() };
125    let mut can_inline = simplify_expression(lhs, ga, cache);
126    can_inline &= simplify_expression(rhs, ga, cache);
127
128    // The folding lives in a separate function: in unoptimized builds its many
129    // `Expression` temporaries would otherwise be part of this function's stack
130    // frame, which is live during the recursion above.
131    let new = fold_binary_expression(*op, lhs, rhs, &mut can_inline);
132    if let Some(new) = new {
133        *expr = new;
134    }
135    can_inline
136}
137
138#[inline(never)]
139fn fold_binary_expression(
140    op: char,
141    lhs: &mut Expression,
142    rhs: &mut Expression,
143    can_inline: &mut bool,
144) -> Option<Expression> {
145    match (op, lhs, rhs) {
146        // constant folding
147        ('+', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
148            Some(Expression::StringLiteral(format_smolstr!("{}{}", a, b)))
149        }
150        ('+', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, _)) => {
151            Some(Expression::NumberLiteral(*a + *b, *un1))
152        }
153        // `LayoutInfo + LayoutInfo` merges layout constraints, mirroring
154        // `impl Add for LayoutInfo` in internal/core/layout.rs. Fold it when
155        // every field of both operands is a number literal; merging only
156        // selects one of the two literals, so the folded value is exactly
157        // what the runtime merge would produce.
158        ('+', Expression::Struct { ty, values: a }, Expression::Struct { values: b, .. })
159            if matches!(ty.name, StructName::Builtin(BuiltinStruct::LayoutInfo)) =>
160        {
161            let ty = ty.clone();
162            ty.fields
163                .keys()
164                .map(|name| {
165                    let Some(Expression::NumberLiteral(x, u)) = a.get(name) else { return None };
166                    let Some(Expression::NumberLiteral(y, _)) = b.get(name) else { return None };
167                    let v = match name.as_str() {
168                        "min" | "min_percent" | "preferred" => x.max(*y),
169                        "max" | "max_percent" | "stretch" => x.min(*y),
170                        _ => return None,
171                    };
172                    Some((name.clone(), Expression::NumberLiteral(v, *u)))
173                })
174                .collect::<Option<_>>()
175                .map(|values| Expression::Struct { ty, values })
176        }
177        ('-', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, _)) => {
178            Some(Expression::NumberLiteral(*a - *b, *un1))
179        }
180        ('*', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
181            if *un1 == Unit::None || *un2 == Unit::None =>
182        {
183            let preserved_unit = if *un1 == Unit::None { *un2 } else { *un1 };
184            Some(Expression::NumberLiteral(*a * *b, preserved_unit))
185        }
186        ('/', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, Unit::None)) => {
187            Some(Expression::NumberLiteral(*a / *b, *un1))
188        }
189        ('/', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
190            if un1 == un2 =>
191        {
192            Some(Expression::NumberLiteral(*a / *b, Unit::None))
193        }
194        // TODO: fold * and / that produce a unit product
195
196        // arithmetic identities
197        ('+', e, Expression::NumberLiteral(n, _))
198        | ('+', Expression::NumberLiteral(n, _), e)
199        | ('-', e, Expression::NumberLiteral(n, _))
200            if *n == 0. =>
201        {
202            Some(std::mem::take(e))
203        }
204        ('*', e, Expression::NumberLiteral(n, Unit::None))
205        | ('*', Expression::NumberLiteral(n, Unit::None), e)
206        | ('/', e, Expression::NumberLiteral(n, Unit::None))
207            if *n == 1. =>
208        {
209            Some(std::mem::take(e))
210        }
211
212        // comparisons
213        (
214            '=' | '!' | '<' | '>' | '≤' | '≥',
215            Expression::NumberLiteral(a, _),
216            Expression::NumberLiteral(b, _),
217        ) => Some(Expression::BoolLiteral(match op {
218            '=' => a == b,
219            '!' => a != b,
220            '<' => a < b,
221            '>' => a > b,
222            '≤' => a <= b,
223            _ => a >= b,
224        })),
225        ('=' | '!', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
226            Some(Expression::BoolLiteral((a == b) == (op == '=')))
227        }
228        ('=' | '!', Expression::EnumerationValue(a), Expression::EnumerationValue(b)) => {
229            Some(Expression::BoolLiteral((a == b) == (op == '=')))
230        }
231        ('=' | '!', Expression::BoolLiteral(a), Expression::BoolLiteral(b)) => {
232            Some(Expression::BoolLiteral((a == b) == (op == '=')))
233        }
234        // TODO: more types and more comparison operators
235
236        // boolean logic
237        ('&', Expression::BoolLiteral(a), Expression::BoolLiteral(b)) => {
238            Some(Expression::BoolLiteral(*a && *b))
239        }
240        ('|', Expression::BoolLiteral(a), Expression::BoolLiteral(b)) => {
241            Some(Expression::BoolLiteral(*a || *b))
242        }
243        ('&', Expression::BoolLiteral(false), _) => {
244            *can_inline = true;
245            Some(Expression::BoolLiteral(false))
246        }
247        ('|', Expression::BoolLiteral(true), _) => {
248            *can_inline = true;
249            Some(Expression::BoolLiteral(true))
250        }
251        ('&', Expression::BoolLiteral(true), e)
252        | ('&', e, Expression::BoolLiteral(true))
253        | ('|', Expression::BoolLiteral(false), e)
254        | ('|', e, Expression::BoolLiteral(false)) => Some(std::mem::take(e)),
255        _ => None,
256    }
257}
258
259#[inline(never)]
260fn simplify_unary_op(
261    expr: &mut Expression,
262    ga: &GlobalAnalysis,
263    cache: &mut ConstPropCache,
264) -> bool {
265    let Expression::UnaryOp { sub, op } = expr else { unreachable!() };
266    let can_inline = simplify_expression(sub, ga, cache);
267    let new = match (*op, &mut **sub) {
268        ('!', Expression::BoolLiteral(b)) => Some(Expression::BoolLiteral(!*b)),
269        ('-', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(-*n, *u)),
270        ('+', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(*n, *u)),
271        _ => None,
272    };
273    if let Some(new) = new {
274        *expr = new;
275    }
276    can_inline
277}
278
279#[inline(never)]
280fn simplify_struct_field_access(
281    expr: &mut Expression,
282    ga: &GlobalAnalysis,
283    cache: &mut ConstPropCache,
284) -> bool {
285    let Expression::StructFieldAccess { base, name } = expr else { unreachable!() };
286    if let Expression::PropertyReference(nr) = &**base
287        && nr.is_constant()
288        && let Some(field_expr) = extract_struct_field_from_constant(nr, name, ga, cache)
289    {
290        *expr = field_expr;
291        return simplify_expression(expr, ga, cache);
292    }
293    let r = simplify_expression(base, ga, cache);
294    if let Expression::Struct { values, .. } = &mut **base
295        && let Some(e) = values.remove(name)
296    {
297        *expr = e;
298        return simplify_expression(expr, ga, cache);
299    }
300    r
301}
302
303#[inline(never)]
304fn simplify_cast(expr: &mut Expression, ga: &GlobalAnalysis, cache: &mut ConstPropCache) -> bool {
305    let Expression::Cast { from, to } = expr else { unreachable!() };
306    let can_inline = simplify_expression(from, ga, cache);
307    let new = if from.ty() == *to {
308        Some(std::mem::take(&mut **from))
309    } else {
310        match (&**from, &*to) {
311            (Expression::NumberLiteral(x, Unit::None), Type::String) => {
312                locale_independent_number_to_string(*x).map(Expression::StringLiteral)
313            }
314            (Expression::NumberLiteral(x, _), Type::Float32) => {
315                Some(Expression::NumberLiteral(*x, Unit::None))
316            }
317            (Expression::Struct { values, .. }, Type::Struct(ty)) => {
318                Some(Expression::Struct { ty: ty.clone(), values: values.clone() })
319            }
320            _ => None,
321        }
322    };
323    if let Some(new) = new {
324        *expr = new;
325    }
326    can_inline
327}
328
329#[inline(never)]
330fn simplify_min_max(
331    expr: &mut Expression,
332    ga: &GlobalAnalysis,
333    cache: &mut ConstPropCache,
334) -> bool {
335    let Expression::MinMax { op, lhs, rhs, ty: _ } = expr else { unreachable!() };
336    let can_inline = simplify_expression(lhs, ga, cache) & simplify_expression(rhs, ga, cache);
337    if let (Expression::NumberLiteral(lhs, u), Expression::NumberLiteral(rhs, _)) = (&**lhs, &**rhs)
338    {
339        let v = match op {
340            MinMaxOp::Min => lhs.min(*rhs),
341            MinMaxOp::Max => lhs.max(*rhs),
342        };
343        *expr = Expression::NumberLiteral(v, *u);
344    }
345    can_inline
346}
347
348#[inline(never)]
349fn simplify_condition(
350    expr: &mut Expression,
351    ga: &GlobalAnalysis,
352    cache: &mut ConstPropCache,
353) -> bool {
354    let Expression::Condition { condition, true_expr, false_expr, .. } = expr else {
355        unreachable!()
356    };
357    let mut can_inline = simplify_expression(condition, ga, cache);
358    can_inline &= match &**condition {
359        Expression::BoolLiteral(true) => {
360            *expr = *true_expr.clone();
361            simplify_expression(expr, ga, cache)
362        }
363        Expression::BoolLiteral(false) => {
364            *expr = *false_expr.clone();
365            simplify_expression(expr, ga, cache)
366        }
367        _ => simplify_expression(true_expr, ga, cache) & simplify_expression(false_expr, ga, cache),
368    };
369    can_inline
370}
371
372#[inline(never)]
373fn simplify_single_statement_code_block(
374    expr: &mut Expression,
375    ga: &GlobalAnalysis,
376    cache: &mut ConstPropCache,
377) -> bool {
378    let Expression::CodeBlock(stmts) = expr else { unreachable!() };
379    *expr = stmts[0].clone();
380    simplify_expression(expr, ga, cache)
381}
382
383#[inline(never)]
384fn simplify_function_call(
385    expr: &mut Expression,
386    ga: &GlobalAnalysis,
387    cache: &mut ConstPropCache,
388) -> bool {
389    let Expression::FunctionCall { function, arguments, .. } = expr else { unreachable!() };
390    let mut args_can_inline = true;
391    for arg in arguments.iter_mut() {
392        args_can_inline &= simplify_expression(arg, ga, cache);
393    }
394    if args_can_inline && let Some(inlined) = try_inline_function(function, arguments, ga, cache) {
395        *expr = inlined;
396        return true;
397    }
398    false
399}
400
401/// Will extract the property binding from the given named reference
402/// and propagate constant expression within it. If that's possible,
403/// return the new expression. Results are cached per NamedReference.
404fn extract_constant_property_reference(
405    nr: &NamedReference,
406    ga: &GlobalAnalysis,
407    cache: &mut ConstPropCache,
408) -> Option<Expression> {
409    debug_assert!(nr.is_constant());
410    if let Some(cached) = cache.get(nr) {
411        return cached.clone();
412    }
413    let result = extract_constant_property_reference_impl(nr, ga, cache);
414    cache.insert(nr.clone(), result.clone());
415    result
416}
417
418/// Extract just one field from a constant struct property, cloning only that
419/// field instead of the entire struct expression.
420fn extract_struct_field_from_constant(
421    nr: &NamedReference,
422    field_name: &str,
423    ga: &GlobalAnalysis,
424    cache: &mut ConstPropCache,
425) -> Option<Expression> {
426    // Populate the cache via the canonical path (result itself is discarded)
427    let _ = extract_constant_property_reference(nr, ga, cache);
428    if let Some(Some(Expression::Struct { values, .. })) = cache.get(nr) {
429        values.get(field_name).cloned()
430    } else {
431        None
432    }
433}
434
435fn extract_constant_property_reference_impl(
436    nr: &NamedReference,
437    ga: &GlobalAnalysis,
438    cache: &mut ConstPropCache,
439) -> Option<Expression> {
440    // find the binding.
441    let mut element = nr.element();
442    let mut expression = loop {
443        if let Some(binding) = element.borrow().binding(nr.name()) {
444            if !binding.two_way_bindings.is_empty() {
445                // TODO: In practice, we should still find out what the real binding is
446                // and solve that.
447                return None;
448            }
449            if !matches!(binding.value_expression(), Expression::Invalid) {
450                break binding.expression.clone();
451            }
452        };
453        if let Some(decl) = element.clone().borrow().property_declarations.get(nr.name()) {
454            if let Some(alias) = &decl.is_alias {
455                return extract_constant_property_reference(alias, ga, cache);
456            }
457        } else if let ElementType::Component(c) = &element.clone().borrow().base_type {
458            element = c.root_element.clone();
459            continue;
460        }
461
462        // There is no binding for this property, return the default value
463        let ty = nr.ty();
464        debug_assert!(!matches!(ty, Type::Invalid));
465        return Some(Expression::default_value_for_type(&ty));
466    };
467    if !(simplify_expression(&mut expression, ga, cache)) {
468        return None;
469    }
470    Some(expression)
471}
472
473fn try_inline_function(
474    function: &Callable,
475    arguments: &[Expression],
476    ga: &GlobalAnalysis,
477    cache: &mut ConstPropCache,
478) -> Option<Expression> {
479    let function = match function {
480        Callable::Function(function) => function,
481        Callable::Builtin(b) => return try_inline_builtin_function(b, arguments, ga),
482        _ => return None,
483    };
484    if !function.is_constant() {
485        return None;
486    }
487    let mut body = extract_constant_property_reference(function, ga, cache)?;
488
489    fn substitute_arguments_recursive(e: &mut Expression, arguments: &[Expression]) {
490        if let Expression::FunctionParameterReference { index, ty } = e {
491            let e_new = arguments.get(*index).expect("reference to invalid arg").clone();
492            debug_assert_eq!(e_new.ty(), *ty);
493            *e = e_new;
494        } else {
495            e.visit_mut(|e| substitute_arguments_recursive(e, arguments));
496        }
497    }
498    substitute_arguments_recursive(&mut body, arguments);
499
500    if simplify_expression(&mut body, ga, cache) { Some(body) } else { None }
501}
502
503fn try_inline_builtin_function(
504    b: &BuiltinFunction,
505    args: &[Expression],
506    ga: &GlobalAnalysis,
507) -> Option<Expression> {
508    let a = |idx: usize| -> Option<f64> {
509        match args.get(idx)? {
510            Expression::NumberLiteral(n, Unit::None) => Some(*n),
511            _ => None,
512        }
513    };
514    let num = |n: f64| Some(Expression::NumberLiteral(n, Unit::None));
515
516    match b {
517        BuiltinFunction::GetWindowScaleFactor => {
518            ga.const_scale_factor.map(|factor| Expression::NumberLiteral(factor as _, Unit::None))
519        }
520        BuiltinFunction::GetWindowDefaultFontSize => match ga.default_font_size {
521            crate::passes::binding_analysis::DefaultFontSize::LogicalValue(val) => {
522                Some(Expression::NumberLiteral(val as _, Unit::Px))
523            }
524            _ => None,
525        },
526        BuiltinFunction::Mod => num(a(0)?.rem_euclid(a(1)?)),
527        BuiltinFunction::Round => num(a(0)?.round()),
528        BuiltinFunction::Ceil => num(a(0)?.ceil()),
529        BuiltinFunction::Floor => num(a(0)?.floor()),
530        BuiltinFunction::Abs => num(a(0)?.abs()),
531        BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => {
532            let Some(Expression::StringLiteral(s)) = args.first() else { return None };
533            // Only fold when the string can't contain the decimal separator of any locale,
534            // so that parsing gives the same result regardless of the locale.
535            if !s.chars().all(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | 'e' | 'E')) {
536                return None;
537            }
538            let value = s.parse::<f32>().ok();
539            Some(match b {
540                BuiltinFunction::StringToFloat => {
541                    Expression::NumberLiteral(value.unwrap_or(0.) as f64, Unit::None)
542                }
543                _ => Expression::BoolLiteral(value.is_some()),
544            })
545        }
546        _ => None,
547    }
548}
549
550#[test]
551fn test() {
552    let mut compiler_config =
553        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
554    compiler_config.style = Some("fluent".into());
555    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
556    let doc_node = crate::parser::parse(
557        r#"
558/* ... */
559struct Hello { s: string, v: float }
560enum Enum { aa, bb, cc }
561global G {
562    pure function complicated(a: float ) -> bool { if a > 5 { return true; }; if a < 1 { return true; }; uncomplicated() }
563    pure function uncomplicated( ) -> bool { false }
564    out property <float> p : 3 * 2 + 15 ;
565    property <string> q: "foo " + 42;
566    out property <float> w : -p / 2;
567    out property <Hello> out: { s: q, v: complicated(w + 15) ? -123 : p };
568
569    in-out property <Enum> e: Enum.bb;
570}
571export component Foo {
572    in property <int> input;
573    out property<float> out1: G.w;
574    out property<float> out2: G.out.v;
575    out property<bool> out3: false ? input == 12 : input > 0 ? input == 11 : G.e == Enum.bb;
576}
577"#
578        .into(),
579        Some(std::path::Path::new("HELLO")),
580        &mut test_diags,
581    );
582    let (doc, diag, _) =
583        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
584    assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
585
586    let expected_p = 3.0 * 2.0 + 15.0;
587    let expected_w = -expected_p / 2.0;
588    let root_element = doc.inner_components.last().unwrap().root_element.clone();
589    let out1_binding = root_element.borrow().binding("out1").unwrap().expression.clone();
590    match &out1_binding {
591        Expression::NumberLiteral(n, _) => assert_eq!(*n, expected_w),
592        _ => panic!("not number {out1_binding:?}"),
593    }
594    let out2_binding = root_element.borrow().binding("out2").unwrap().expression.clone();
595    match &out2_binding {
596        Expression::NumberLiteral(n, _) => assert_eq!(*n, expected_p),
597        _ => panic!("not number {out2_binding:?}"),
598    }
599    let out3_binding = root_element.borrow().binding("out3").unwrap().expression.clone();
600    match &out3_binding {
601        // We have a code block because the first entry stores the value of `input` in a local variable
602        Expression::CodeBlock(stmts) => match &stmts[1] {
603            Expression::Condition { condition: _, true_expr: _, false_expr, .. } => {
604                match &**false_expr {
605                    Expression::BoolLiteral(b) => assert!(*b),
606                    _ => panic!("false_expr not optimized in : {out3_binding:?}"),
607                }
608            }
609            _ => panic!("not condition:  {out3_binding:?}"),
610        },
611        _ => panic!("not code block: {out3_binding:?}"),
612    };
613}
614
615#[test]
616fn test_locale_dependent_string_conversion() {
617    let mut compiler_config =
618        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
619    compiler_config.style = Some("fluent".into());
620    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
621    let doc_node = crate::parser::parse(
622        r#"
623export component Foo {
624    out property <string> int-str: "n=" + 42;
625    out property <string> calc-str: "n=" + (6 * 7);
626    out property <string> float-str: "n=" + 4.5;
627    out property <float> int-float: "42".to-float();
628    out property <bool> int-is-float: "42".is-float();
629    out property <float> frac-float: "4,2".to-float();
630}
631"#
632        .into(),
633        Some(std::path::Path::new("HELLO")),
634        &mut test_diags,
635    );
636    let (doc, diag, _) =
637        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
638    assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
639
640    let root_element = doc.inner_components.last().unwrap().root_element.clone();
641    let binding = |name: &str| root_element.borrow().binding(name).unwrap().clone();
642    let is_const = |name: &str| {
643        root_element.borrow().binding(name).unwrap().analysis.as_ref().unwrap().is_const
644    };
645
646    // Conversions whose result contains no decimal separator are folded and stay constant
647    assert!(
648        matches!(&binding("int-str").expression, Expression::StringLiteral(s) if s == "n=42"),
649        "{:?}",
650        binding("int-str").expression
651    );
652    assert!(is_const("int-str"));
653    assert!(matches!(&binding("calc-str").expression, Expression::StringLiteral(s) if s == "n=42"));
654    assert!(is_const("calc-str"));
655    assert!(
656        matches!(&binding("int-float").expression, Expression::NumberLiteral(n, _) if *n == 42.)
657    );
658    assert!(is_const("int-float"));
659    assert!(matches!(&binding("int-is-float").expression, Expression::BoolLiteral(true)));
660    assert!(is_const("int-is-float"));
661
662    // Locale-dependent conversions are not folded and their bindings are no longer constant,
663    // so that they are re-evaluated when the locale changes at runtime
664    assert!(
665        !matches!(&binding("float-str").expression, Expression::StringLiteral(_)),
666        "{:?}",
667        binding("float-str").expression
668    );
669    assert!(!is_const("float-str"));
670    assert!(matches!(&binding("frac-float").expression, Expression::FunctionCall { .. }));
671    assert!(!is_const("frac-float"));
672}
673
674#[test]
675fn test_propagate_font_size() {
676    struct Case {
677        default_font_size: &'static str,
678        another_window: &'static str,
679        check_expression: fn(&Expression),
680    }
681
682    #[track_caller]
683    fn assert_expr_is_mul(e: &Expression, l: f64, r: f64) {
684        assert!(
685            matches!(e, Expression::Cast { from, .. }
686                        if matches!(from.as_ref(), Expression::BinaryExpression { lhs, rhs, op: '*', ..}
687                        if matches!((lhs.as_ref(), rhs.as_ref()), (Expression::NumberLiteral(lhs, _), Expression::NumberLiteral(rhs, _)) if *lhs == l && *rhs == r ))),
688            "Expression {e:?} is not a {l} * {r} expected"
689        );
690    }
691
692    for Case { default_font_size, another_window, check_expression } in [
693        Case {
694            default_font_size: "default-font-size: 12px;",
695            another_window: "",
696            check_expression: |e| assert_expr_is_mul(e, 5.0, 12.0),
697        },
698        Case {
699            default_font_size: "default-font-size: some-value;",
700            another_window: "",
701            check_expression: |e| {
702                assert!(
703                    !e.is_constant(None),
704                    "{e:?} should not be constant since some-value can vary at runtime"
705                );
706            },
707        },
708        Case {
709            default_font_size: "default-font-size: 25px;",
710            another_window: "export component AnotherWindow inherits Window { default-font-size: 8px; }",
711            check_expression: |e| {
712                assert!(
713                    e.is_constant(None) && !matches!(e, Expression::NumberLiteral(_, _)),
714                    "{e:?} should be constant but not known at compile time since there are two windows"
715                );
716            },
717        },
718        Case {
719            default_font_size: "default-font-size: 25px;",
720            another_window: "export component AnotherWindow inherits Window { }",
721            check_expression: |e| {
722                assert!(
723                    !e.is_constant(None),
724                    "should not be const since at least one window has it unset"
725                );
726            },
727        },
728        Case {
729            default_font_size: "default-font-size: 20px;",
730            another_window: "export component AnotherWindow inherits Window { default-font-size: 20px;  }",
731            check_expression: |e| assert_expr_is_mul(e, 5.0, 20.0),
732        },
733        Case {
734            default_font_size: "default-font-size: 20px;",
735            another_window: "export component AnotherWindow inherits Window { in property <float> f: 1; default-font-size: 20px*f;  }",
736            check_expression: |e| {
737                assert!(
738                    !e.is_constant(None),
739                    "{e:?} should not be constant since 'f' can vary at runtime"
740                );
741            },
742        },
743    ] {
744        let source = format!(
745            r#"
746component SomeComponent {{
747    in-out property <length> rem-prop: 5rem;
748}}
749
750{another_window}
751
752export component Foo inherits Window {{
753    in property <length> some-value: 45px;
754    {default_font_size}
755    sc1 := SomeComponent {{}}
756    sc2 := SomeComponent {{}}
757
758    out property <length> test: sc1.rem-prop;
759}}
760"#
761        );
762
763        let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
764
765        let doc_node = crate::parser::parse(
766            source.clone(),
767            Some(std::path::Path::new("HELLO")),
768            &mut test_diags,
769        );
770        let mut compiler_config =
771            crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
772        compiler_config.style = Some("fluent".into());
773        let (doc, diag, _) =
774            spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
775        assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
776
777        let root_element = doc.inner_components.last().unwrap().root_element.clone();
778        let out1_binding = root_element.borrow().binding("test").unwrap().expression.clone();
779        check_expression(&out1_binding);
780    }
781}
782
783#[test]
784fn test_const_scale_factor() {
785    let source = r#"
786export component Foo inherits Window {
787    out property <length> test: 10phx;
788}"#;
789
790    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
791    let doc_node = crate::parser::parse(
792        source.to_string(),
793        Some(std::path::Path::new("HELLO")),
794        &mut test_diags,
795    );
796    let mut compiler_config =
797        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
798    compiler_config.style = Some("fluent".into());
799    compiler_config.const_scale_factor = Some(2.);
800    let (doc, diag, _) =
801        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
802    assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
803
804    let root_element = doc.inner_components.last().unwrap().root_element.clone();
805    let mut test_binding = root_element.borrow().binding("test").unwrap().expression.clone();
806    if let Expression::Cast { from, to: _ } = test_binding {
807        test_binding = *from;
808    }
809    assert!(
810        matches!(test_binding, Expression::NumberLiteral(val, _) if val == 5.0),
811        "Expression should be 5.0: {test_binding:?}"
812    );
813}
814
815#[test]
816fn test_unit_normalization() {
817    // Compile `out property <ty> a: expr;` and return the folded binding of `a`.
818    fn fold(ty: &str, expr: &str) -> Expression {
819        let mut config =
820            crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
821        config.style = Some("fluent".into());
822        let mut diags = crate::diagnostics::BuildDiagnostics::default();
823        let doc_node = crate::parser::parse(
824            format!("export component Foo {{ out property <{ty}> a: {expr}; }}").into(),
825            Some(std::path::Path::new("HELLO")),
826            &mut diags,
827        );
828        let (doc, diag, _) = spin_on::spin_on(crate::compile_syntax_node(doc_node, diags, config));
829        assert!(!diag.has_errors(), "{expr}: {:#?}", diag.to_string_vec());
830        let root_element = doc.inner_components.last().unwrap().root_element.clone();
831        root_element.borrow().binding("a").unwrap().expression.clone()
832    }
833
834    // A literal is stored in its type's canonical unit, not the one it was written in.
835    assert!(matches!(fold("length", "1in"), Expression::NumberLiteral(v, Unit::Px) if v == 96.0));
836    assert!(
837        matches!(fold("duration", "2s"), Expression::NumberLiteral(v, Unit::Ms) if v == 2000.0)
838    );
839
840    // Mixed units of one type now fold, because they share a canonical unit.
841    assert!(
842        matches!(fold("length", "5px + 5cm"), Expression::NumberLiteral(v, Unit::Px) if v == 194.0),
843        "{:?}",
844        fold("length", "5px + 5cm")
845    );
846    assert!(
847        matches!(fold("duration", "1s - 500ms"), Expression::NumberLiteral(v, Unit::Ms) if v == 500.0)
848    );
849    assert!(
850        matches!(fold("length", "max(12cm, 12px)"), Expression::NumberLiteral(v, Unit::Px) if v == 12.0 * 37.8),
851        "{:?}",
852        fold("length", "max(12cm, 12px)")
853    );
854
855    // Comparisons fold on the normalized values, so different units compare correctly.
856    assert!(matches!(fold("bool", "12cm == 12px"), Expression::BoolLiteral(false)));
857    assert!(matches!(fold("bool", "1s == 1000ms"), Expression::BoolLiteral(true)));
858    assert!(matches!(fold("bool", "12cm < 12px"), Expression::BoolLiteral(false)));
859    assert!(matches!(fold("bool", "1turn == 360deg"), Expression::BoolLiteral(true)));
860
861    // Multiplying by a unitless 0 yields 0 in the other factor's unit.
862    assert!(
863        matches!(fold("length", "10px * 0.0"), Expression::NumberLiteral(v, Unit::Px) if v == 0.0)
864    );
865    assert!(
866        matches!(fold("float", "10 * 0.0"), Expression::NumberLiteral(v, Unit::None) if v == 0.0)
867    );
868
869    // Dividing equal units cancels to a unitless ratio.
870    assert!(
871        matches!(fold("float", "3px / 6px"), Expression::NumberLiteral(v, Unit::None) if v == 0.5)
872    );
873    // Equality folds for numbers (now via the ordering arm) and bools.
874    assert!(matches!(fold("bool", "1px != 2px"), Expression::BoolLiteral(true)));
875    assert!(matches!(fold("bool", "true == false"), Expression::BoolLiteral(false)));
876}
877
878#[test]
879fn test_fold_layout_info_merge() {
880    use smol_str::SmolStr;
881    let ty = crate::typeregister::layout_info_type();
882    let info = |min: f64, max: f64, preferred: f64, stretch: f64| Expression::Struct {
883        ty: ty.clone(),
884        values: IntoIterator::into_iter([
885            ("min", Expression::NumberLiteral(min, Unit::Px)),
886            ("max", Expression::NumberLiteral(max, Unit::Px)),
887            ("preferred", Expression::NumberLiteral(preferred, Unit::Px)),
888            ("min_percent", Expression::NumberLiteral(0., Unit::None)),
889            ("max_percent", Expression::NumberLiteral(100., Unit::None)),
890            ("stretch", Expression::NumberLiteral(stretch, Unit::None)),
891        ])
892        .map(|(k, v)| (SmolStr::new_static(k), v))
893        .collect(),
894    };
895
896    let mut expr = Expression::BinaryExpression {
897        lhs: Box::new(info(10., 200., 50., 1.)),
898        rhs: Box::new(info(20., 100., 30., 0.)),
899        op: '+',
900        source_location: None,
901    };
902    fold_const_expression(&mut expr);
903    // The merge takes the max of the lower bounds and the preferred size,
904    // and the min of the upper bounds and the stretch.
905    let Expression::Struct { values, .. } = expr else { panic!("not folded: {expr:?}") };
906    let field = |name: &str| match values.get(name) {
907        Some(Expression::NumberLiteral(v, _)) => *v,
908        other => panic!("field {name} not a literal: {other:?}"),
909    };
910    assert_eq!(field("min"), 20.);
911    assert_eq!(field("max"), 100.);
912    assert_eq!(field("preferred"), 50.);
913    assert_eq!(field("stretch"), 0.);
914
915    // A non-literal field keeps the merge unfolded.
916    let non_literal = Expression::Struct {
917        ty: ty.clone(),
918        values: IntoIterator::into_iter([(
919            SmolStr::new_static("min"),
920            Expression::FunctionParameterReference { index: 0, ty: Type::LogicalLength },
921        )])
922        .collect(),
923    };
924    let mut expr = Expression::BinaryExpression {
925        lhs: Box::new(info(10., 200., 50., 1.)),
926        rhs: Box::new(non_literal),
927        op: '+',
928        source_location: None,
929    };
930    fold_const_expression(&mut expr);
931    assert!(matches!(expr, Expression::BinaryExpression { .. }), "{expr:?}");
932}