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 super::GlobalAnalysis;
7use crate::expression_tree::*;
8use crate::langtype::{BuiltinPrivateStruct, ElementType, StructName, Type};
9use crate::object_tree::*;
10use smol_str::{ToSmolStr, format_smolstr};
11
12pub fn const_propagation(component: &Component, global_analysis: &GlobalAnalysis) {
13    visit_all_expressions(component, |expr, ty| {
14        if matches!(ty(), Type::Callback { .. }) {
15            return;
16        }
17        simplify_expression(expr, global_analysis);
18    });
19}
20
21/// Returns false if the expression still contains a reference to an element
22fn simplify_expression(expr: &mut Expression, ga: &GlobalAnalysis) -> bool {
23    match expr {
24        Expression::PropertyReference(nr) => {
25            if nr.is_constant()
26                && !match nr.ty() {
27                    Type::Struct(s) => {
28                        matches!(
29                            s.name,
30                            StructName::BuiltinPrivate(BuiltinPrivateStruct::StateInfo)
31                        )
32                    }
33                    _ => false,
34                }
35            {
36                // Inline the constant value
37                if let Some(result) = extract_constant_property_reference(nr, ga) {
38                    *expr = result;
39                    return true;
40                }
41            }
42            false
43        }
44        Expression::BinaryExpression { lhs, op, rhs } => {
45            let mut can_inline = simplify_expression(lhs, ga);
46            can_inline &= simplify_expression(rhs, ga);
47
48            let new = match (*op, &mut **lhs, &mut **rhs) {
49                ('+', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
50                    Some(Expression::StringLiteral(format_smolstr!("{}{}", a, b)))
51                }
52                ('+', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
53                    if un1 == un2 =>
54                {
55                    Some(Expression::NumberLiteral(*a + *b, *un1))
56                }
57                ('-', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
58                    if un1 == un2 =>
59                {
60                    Some(Expression::NumberLiteral(*a - *b, *un1))
61                }
62                ('*', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
63                    if *un1 == Unit::None || *un2 == Unit::None =>
64                {
65                    let preserved_unit = if *un1 == Unit::None { *un2 } else { *un1 };
66                    Some(Expression::NumberLiteral(*a * *b, preserved_unit))
67                }
68                (
69                    '/',
70                    Expression::NumberLiteral(a, un1),
71                    Expression::NumberLiteral(b, Unit::None),
72                ) => Some(Expression::NumberLiteral(*a / *b, *un1)),
73                // TODO: take care of * and / when both numbers have units
74                ('=' | '!', Expression::NumberLiteral(a, _), Expression::NumberLiteral(b, _)) => {
75                    Some(Expression::BoolLiteral((a == b) == (*op == '=')))
76                }
77                ('=' | '!', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
78                    Some(Expression::BoolLiteral((a == b) == (*op == '=')))
79                }
80                ('=' | '!', Expression::EnumerationValue(a), Expression::EnumerationValue(b)) => {
81                    Some(Expression::BoolLiteral((a == b) == (*op == '=')))
82                }
83                // TODO: more types and more comparison operators
84                ('&', Expression::BoolLiteral(false), _) => {
85                    can_inline = true;
86                    Some(Expression::BoolLiteral(false))
87                }
88                ('&', _, Expression::BoolLiteral(false)) => {
89                    can_inline = true;
90                    Some(Expression::BoolLiteral(false))
91                }
92                ('&', Expression::BoolLiteral(true), e) => Some(std::mem::take(e)),
93                ('&', e, Expression::BoolLiteral(true)) => Some(std::mem::take(e)),
94                ('|', Expression::BoolLiteral(true), _) => {
95                    can_inline = true;
96                    Some(Expression::BoolLiteral(true))
97                }
98                ('|', _, Expression::BoolLiteral(true)) => {
99                    can_inline = true;
100                    Some(Expression::BoolLiteral(true))
101                }
102                ('|', Expression::BoolLiteral(false), e) => Some(std::mem::take(e)),
103                ('|', e, Expression::BoolLiteral(false)) => Some(std::mem::take(e)),
104                ('>', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
105                    if un1 == un2 =>
106                {
107                    Some(Expression::BoolLiteral(*a > *b))
108                }
109                ('<', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
110                    if un1 == un2 =>
111                {
112                    Some(Expression::BoolLiteral(*a < *b))
113                }
114                _ => None,
115            };
116            if let Some(new) = new {
117                *expr = new;
118            }
119            can_inline
120        }
121        Expression::UnaryOp { sub, op } => {
122            let can_inline = simplify_expression(sub, ga);
123            let new = match (*op, &mut **sub) {
124                ('!', Expression::BoolLiteral(b)) => Some(Expression::BoolLiteral(!*b)),
125                ('-', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(-*n, *u)),
126                ('+', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(*n, *u)),
127                _ => None,
128            };
129            if let Some(new) = new {
130                *expr = new;
131            }
132            can_inline
133        }
134        Expression::StructFieldAccess { base, name } => {
135            let r = simplify_expression(base, ga);
136            if let Expression::Struct { values, .. } = &mut **base
137                && let Some(e) = values.remove(name)
138            {
139                *expr = e;
140                return simplify_expression(expr, ga);
141            }
142            r
143        }
144        Expression::Cast { from, to } => {
145            let can_inline = simplify_expression(from, ga);
146            let new = if from.ty() == *to {
147                Some(std::mem::take(&mut **from))
148            } else {
149                match (&**from, to) {
150                    (Expression::NumberLiteral(x, Unit::None), Type::String) => {
151                        Some(Expression::StringLiteral(x.to_smolstr()))
152                    }
153                    (Expression::Struct { values, .. }, Type::Struct(ty)) => {
154                        Some(Expression::Struct { ty: ty.clone(), values: values.clone() })
155                    }
156                    _ => None,
157                }
158            };
159            if let Some(new) = new {
160                *expr = new;
161            }
162            can_inline
163        }
164        Expression::MinMax { op, lhs, rhs, ty: _ } => {
165            let can_inline = simplify_expression(lhs, ga) & simplify_expression(rhs, ga);
166            if let (Expression::NumberLiteral(lhs, u), Expression::NumberLiteral(rhs, _)) =
167                (&**lhs, &**rhs)
168            {
169                let v = match op {
170                    MinMaxOp::Min => lhs.min(*rhs),
171                    MinMaxOp::Max => lhs.max(*rhs),
172                };
173                *expr = Expression::NumberLiteral(v, *u);
174            }
175            can_inline
176        }
177        Expression::Condition { condition, true_expr, false_expr } => {
178            let mut can_inline = simplify_expression(condition, ga);
179            can_inline &= match &**condition {
180                Expression::BoolLiteral(true) => {
181                    *expr = *true_expr.clone();
182                    simplify_expression(expr, ga)
183                }
184                Expression::BoolLiteral(false) => {
185                    *expr = *false_expr.clone();
186                    simplify_expression(expr, ga)
187                }
188                _ => simplify_expression(true_expr, ga) & simplify_expression(false_expr, ga),
189            };
190            can_inline
191        }
192        // disable this simplification for store local variable, as "let" is not an expression in rust
193        Expression::CodeBlock(stmts)
194            if stmts.len() == 1 && !matches!(stmts[0], Expression::StoreLocalVariable { .. }) =>
195        {
196            *expr = stmts[0].clone();
197            simplify_expression(expr, ga)
198        }
199        Expression::FunctionCall { function, arguments, .. } => {
200            let mut args_can_inline = true;
201            for arg in arguments.iter_mut() {
202                args_can_inline &= simplify_expression(arg, ga);
203            }
204            if args_can_inline && let Some(inlined) = try_inline_function(function, arguments, ga) {
205                *expr = inlined;
206                return true;
207            }
208            false
209        }
210        Expression::ElementReference { .. } => false,
211        Expression::LayoutCacheAccess { .. } => false,
212        Expression::OrganizeGridLayout { .. } => false,
213        Expression::SolveLayout { .. } => false,
214        Expression::ComputeLayoutInfo { .. } => false,
215        _ => {
216            let mut result = true;
217            expr.visit_mut(|expr| result &= simplify_expression(expr, ga));
218            result
219        }
220    }
221}
222
223/// Will extract the property binding from the given named reference
224/// and propagate constant expression within it. If that's possible,
225/// return the new expression
226fn extract_constant_property_reference(
227    nr: &NamedReference,
228    ga: &GlobalAnalysis,
229) -> Option<Expression> {
230    debug_assert!(nr.is_constant());
231    // find the binding.
232    let mut element = nr.element();
233    let mut expression = loop {
234        if let Some(binding) = element.borrow().bindings.get(nr.name()) {
235            let binding = binding.borrow();
236            if !binding.two_way_bindings.is_empty() {
237                // TODO: In practice, we should still find out what the real binding is
238                // and solve that.
239                return None;
240            }
241            if !matches!(binding.expression, Expression::Invalid) {
242                break binding.expression.clone();
243            }
244        };
245        if let Some(decl) = element.clone().borrow().property_declarations.get(nr.name()) {
246            if let Some(alias) = &decl.is_alias {
247                return extract_constant_property_reference(alias, ga);
248            }
249        } else if let ElementType::Component(c) = &element.clone().borrow().base_type {
250            element = c.root_element.clone();
251            continue;
252        }
253
254        // There is no binding for this property, return the default value
255        let ty = nr.ty();
256        debug_assert!(!matches!(ty, Type::Invalid));
257        return Some(Expression::default_value_for_type(&ty));
258    };
259    if !(simplify_expression(&mut expression, ga)) {
260        return None;
261    }
262    Some(expression)
263}
264
265fn try_inline_function(
266    function: &Callable,
267    arguments: &[Expression],
268    ga: &GlobalAnalysis,
269) -> Option<Expression> {
270    let function = match function {
271        Callable::Function(function) => function,
272        Callable::Builtin(b) => return try_inline_builtin_function(b, arguments, ga),
273        _ => return None,
274    };
275    if !function.is_constant() {
276        return None;
277    }
278    let mut body = extract_constant_property_reference(function, ga)?;
279
280    fn substitute_arguments_recursive(e: &mut Expression, arguments: &[Expression]) {
281        if let Expression::FunctionParameterReference { index, ty } = e {
282            let e_new = arguments.get(*index).expect("reference to invalid arg").clone();
283            debug_assert_eq!(e_new.ty(), *ty);
284            *e = e_new;
285        } else {
286            e.visit_mut(|e| substitute_arguments_recursive(e, arguments));
287        }
288    }
289    substitute_arguments_recursive(&mut body, arguments);
290
291    if simplify_expression(&mut body, ga) { Some(body) } else { None }
292}
293
294fn try_inline_builtin_function(
295    b: &BuiltinFunction,
296    args: &[Expression],
297    ga: &GlobalAnalysis,
298) -> Option<Expression> {
299    let a = |idx: usize| -> Option<f64> {
300        match args.get(idx)? {
301            Expression::NumberLiteral(n, Unit::None) => Some(*n),
302            _ => None,
303        }
304    };
305    let num = |n: f64| Some(Expression::NumberLiteral(n, Unit::None));
306
307    match b {
308        BuiltinFunction::GetWindowScaleFactor => {
309            ga.const_scale_factor.map(|factor| Expression::NumberLiteral(factor as _, Unit::None))
310        }
311        BuiltinFunction::GetWindowDefaultFontSize => match ga.default_font_size {
312            crate::passes::binding_analysis::DefaultFontSize::LogicalValue(val) => {
313                Some(Expression::NumberLiteral(val as _, Unit::Px))
314            }
315            _ => None,
316        },
317        BuiltinFunction::Mod => num(a(0)?.rem_euclid(a(1)?)),
318        BuiltinFunction::Round => num(a(0)?.round()),
319        BuiltinFunction::Ceil => num(a(0)?.ceil()),
320        BuiltinFunction::Floor => num(a(0)?.floor()),
321        BuiltinFunction::Abs => num(a(0)?.abs()),
322        _ => None,
323    }
324}
325
326#[test]
327fn test() {
328    let mut compiler_config =
329        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
330    compiler_config.style = Some("fluent".into());
331    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
332    let doc_node = crate::parser::parse(
333        r#"
334/* ... */
335struct Hello { s: string, v: float }
336enum Enum { aa, bb, cc }
337global G {
338    pure function complicated(a: float ) -> bool { if a > 5 { return true; }; if a < 1 { return true; }; uncomplicated() }
339    pure function uncomplicated( ) -> bool { false }
340    out property <float> p : 3 * 2 + 15 ;
341    property <string> q: "foo " + 42;
342    out property <float> w : -p / 2;
343    out property <Hello> out: { s: q, v: complicated(w + 15) ? -123 : p };
344
345    in-out property <Enum> e: Enum.bb;
346}
347export component Foo {
348    in property <int> input;
349    out property<float> out1: G.w;
350    out property<float> out2: G.out.v;
351    out property<bool> out3: false ? input == 12 : input > 0 ? input == 11 : G.e == Enum.bb;
352}
353"#
354        .into(),
355        Some(std::path::Path::new("HELLO")),
356        &mut test_diags,
357    );
358    let (doc, diag, _) =
359        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
360    assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
361
362    let expected_p = 3.0 * 2.0 + 15.0;
363    let expected_w = -expected_p / 2.0;
364    let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
365    let out1_binding = bindings.get("out1").unwrap().borrow().expression.clone();
366    match &out1_binding {
367        Expression::NumberLiteral(n, _) => assert_eq!(*n, expected_w),
368        _ => panic!("not number {out1_binding:?}"),
369    }
370    let out2_binding = bindings.get("out2").unwrap().borrow().expression.clone();
371    match &out2_binding {
372        Expression::NumberLiteral(n, _) => assert_eq!(*n, expected_p),
373        _ => panic!("not number {out2_binding:?}"),
374    }
375    let out3_binding = bindings.get("out3").unwrap().borrow().expression.clone();
376    match &out3_binding {
377        // We have a code block because the first entry stores the value of `intput` in a local variable
378        Expression::CodeBlock(stmts) => match &stmts[1] {
379            Expression::Condition { condition: _, true_expr: _, false_expr } => match &**false_expr
380            {
381                Expression::BoolLiteral(b) => assert_eq!(*b, true),
382                _ => panic!("false_expr not optimized in : {out3_binding:?}"),
383            },
384            _ => panic!("not condition:  {out3_binding:?}"),
385        },
386        _ => panic!("not code block: {out3_binding:?}"),
387    };
388}
389
390#[test]
391fn test_propagate_font_size() {
392    struct Case {
393        default_font_size: &'static str,
394        another_window: &'static str,
395        check_expression: fn(&Expression),
396    }
397
398    #[track_caller]
399    fn assert_expr_is_mul(e: &Expression, l: f64, r: f64) {
400        assert!(
401            matches!(e, Expression::Cast { from, .. }
402                        if matches!(from.as_ref(), Expression::BinaryExpression { lhs, rhs, op: '*'}
403                        if matches!((lhs.as_ref(), rhs.as_ref()), (Expression::NumberLiteral(lhs, _), Expression::NumberLiteral(rhs, _)) if *lhs == l && *rhs == r ))),
404            "Expression {e:?} is not a {l} * {r} expected"
405        );
406    }
407
408    for Case { default_font_size, another_window, check_expression } in [
409        Case {
410            default_font_size: "default-font-size: 12px;",
411            another_window: "",
412            check_expression: |e| assert_expr_is_mul(e, 5.0, 12.0),
413        },
414        Case {
415            default_font_size: "default-font-size: some-value;",
416            another_window: "",
417            check_expression: |e| {
418                assert!(
419                    !e.is_constant(None),
420                    "{e:?} should not be constant since some-value can vary at runtime"
421                );
422            },
423        },
424        Case {
425            default_font_size: "default-font-size: 25px;",
426            another_window: "export component AnotherWindow inherits Window { default-font-size: 8px; }",
427            check_expression: |e| {
428                assert!(
429                    e.is_constant(None) && !matches!(e, Expression::NumberLiteral(_, _)),
430                    "{e:?} should be constant but not known at compile time since there are two windows"
431                );
432            },
433        },
434        Case {
435            default_font_size: "default-font-size: 25px;",
436            another_window: "export component AnotherWindow inherits Window { }",
437            check_expression: |e| {
438                assert!(
439                    !e.is_constant(None),
440                    "should not be const since at least one window has it unset"
441                );
442            },
443        },
444        Case {
445            default_font_size: "default-font-size: 20px;",
446            another_window: "export component AnotherWindow inherits Window { default-font-size: 20px;  }",
447            check_expression: |e| assert_expr_is_mul(e, 5.0, 20.0),
448        },
449        Case {
450            default_font_size: "default-font-size: 20px;",
451            another_window: "export component AnotherWindow inherits Window { in property <float> f: 1; default-font-size: 20px*f;  }",
452            check_expression: |e| {
453                assert!(
454                    !e.is_constant(None),
455                    "{e:?} should not be constant since 'f' can vary at runtime"
456                );
457            },
458        },
459    ] {
460        let source = format!(
461            r#"
462component SomeComponent {{
463    in-out property <length> rem-prop: 5rem;
464}}
465
466{another_window}
467
468export component Foo inherits Window {{
469    in property <length> some-value: 45px;
470    {default_font_size}
471    sc1 := SomeComponent {{}}
472    sc2 := SomeComponent {{}}
473
474    out property <length> test: sc1.rem-prop;
475}}
476"#
477        );
478
479        let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
480
481        let doc_node = crate::parser::parse(
482            source.clone(),
483            Some(std::path::Path::new("HELLO")),
484            &mut test_diags,
485        );
486        let mut compiler_config =
487            crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
488        compiler_config.style = Some("fluent".into());
489        let (doc, diag, _) =
490            spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
491        assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
492
493        let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
494        let out1_binding = bindings.get("test").unwrap().borrow().expression.clone();
495        check_expression(&out1_binding);
496    }
497}
498
499#[test]
500fn test_const_scale_factor() {
501    let source = r#"
502export component Foo inherits Window {
503    out property <length> test: 10phx;
504}"#;
505
506    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
507    let doc_node = crate::parser::parse(
508        source.to_string(),
509        Some(std::path::Path::new("HELLO")),
510        &mut test_diags,
511    );
512    let mut compiler_config =
513        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
514    compiler_config.style = Some("fluent".into());
515    compiler_config.const_scale_factor = Some(2.);
516    let (doc, diag, _) =
517        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
518    assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
519
520    let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
521    let mut test_binding = bindings.get("test").unwrap().borrow().expression.clone();
522    if let Expression::Cast { from, to: _ } = test_binding {
523        test_binding = *from;
524    }
525    assert!(
526        matches!(test_binding, Expression::NumberLiteral(val, _) if val == 5.0),
527        "Expression should be 5.0: {test_binding:?}"
528    );
529}