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::ElementType;
9use crate::langtype::Type;
10use crate::object_tree::*;
11use smol_str::{format_smolstr, ToSmolStr};
12
13pub fn const_propagation(component: &Component, global_analysis: &GlobalAnalysis) {
14    visit_all_expressions(component, |expr, ty| {
15        if matches!(ty(), Type::Callback { .. }) {
16            return;
17        }
18        simplify_expression(expr, global_analysis);
19    });
20}
21
22/// Returns false if the expression still contains a reference to an element
23fn simplify_expression(expr: &mut Expression, ga: &GlobalAnalysis) -> bool {
24    match expr {
25        Expression::PropertyReference(nr) => {
26            if nr.is_constant()
27                && !match nr.ty() {
28                    Type::Struct(s) => {
29                        s.name.as_ref().is_some_and(|name| name.ends_with("::StateInfo"))
30                    }
31                    _ => false,
32                }
33            {
34                // Inline the constant value
35                if let Some(result) = extract_constant_property_reference(nr, ga) {
36                    *expr = result;
37                    return true;
38                }
39            }
40            false
41        }
42        Expression::BinaryExpression { lhs, op, rhs } => {
43            let mut can_inline = simplify_expression(lhs, ga);
44            can_inline &= simplify_expression(rhs, ga);
45
46            let new = match (*op, &mut **lhs, &mut **rhs) {
47                ('+', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
48                    Some(Expression::StringLiteral(format_smolstr!("{}{}", a, b)))
49                }
50                ('+', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
51                    if un1 == un2 =>
52                {
53                    Some(Expression::NumberLiteral(*a + *b, *un1))
54                }
55                ('-', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
56                    if un1 == un2 =>
57                {
58                    Some(Expression::NumberLiteral(*a - *b, *un1))
59                }
60                ('*', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
61                    if *un1 == Unit::None || *un2 == Unit::None =>
62                {
63                    let preserved_unit = if *un1 == Unit::None { *un2 } else { *un1 };
64                    Some(Expression::NumberLiteral(*a * *b, preserved_unit))
65                }
66                (
67                    '/',
68                    Expression::NumberLiteral(a, un1),
69                    Expression::NumberLiteral(b, Unit::None),
70                ) => Some(Expression::NumberLiteral(*a / *b, *un1)),
71                // TODO: take care of * and / when both numbers have units
72                ('=' | '!', Expression::NumberLiteral(a, _), Expression::NumberLiteral(b, _)) => {
73                    Some(Expression::BoolLiteral((a == b) == (*op == '=')))
74                }
75                ('=' | '!', Expression::StringLiteral(a), Expression::StringLiteral(b)) => {
76                    Some(Expression::BoolLiteral((a == b) == (*op == '=')))
77                }
78                ('=' | '!', Expression::EnumerationValue(a), Expression::EnumerationValue(b)) => {
79                    Some(Expression::BoolLiteral((a == b) == (*op == '=')))
80                }
81                // TODO: more types and more comparison operators
82                ('&', Expression::BoolLiteral(false), _) => {
83                    can_inline = true;
84                    Some(Expression::BoolLiteral(false))
85                }
86                ('&', _, Expression::BoolLiteral(false)) => {
87                    can_inline = true;
88                    Some(Expression::BoolLiteral(false))
89                }
90                ('&', Expression::BoolLiteral(true), e) => Some(std::mem::take(e)),
91                ('&', e, Expression::BoolLiteral(true)) => Some(std::mem::take(e)),
92                ('|', Expression::BoolLiteral(true), _) => {
93                    can_inline = true;
94                    Some(Expression::BoolLiteral(true))
95                }
96                ('|', _, Expression::BoolLiteral(true)) => {
97                    can_inline = true;
98                    Some(Expression::BoolLiteral(true))
99                }
100                ('|', Expression::BoolLiteral(false), e) => Some(std::mem::take(e)),
101                ('|', e, Expression::BoolLiteral(false)) => Some(std::mem::take(e)),
102                ('>', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
103                    if un1 == un2 =>
104                {
105                    Some(Expression::BoolLiteral(*a > *b))
106                }
107                ('<', Expression::NumberLiteral(a, un1), Expression::NumberLiteral(b, un2))
108                    if un1 == un2 =>
109                {
110                    Some(Expression::BoolLiteral(*a < *b))
111                }
112                _ => None,
113            };
114            if let Some(new) = new {
115                *expr = new;
116            }
117            can_inline
118        }
119        Expression::UnaryOp { sub, op } => {
120            let can_inline = simplify_expression(sub, ga);
121            let new = match (*op, &mut **sub) {
122                ('!', Expression::BoolLiteral(b)) => Some(Expression::BoolLiteral(!*b)),
123                ('-', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(-*n, *u)),
124                ('+', Expression::NumberLiteral(n, u)) => Some(Expression::NumberLiteral(*n, *u)),
125                _ => None,
126            };
127            if let Some(new) = new {
128                *expr = new;
129            }
130            can_inline
131        }
132        Expression::StructFieldAccess { base, name } => {
133            let r = simplify_expression(base, ga);
134            if let Expression::Struct { values, .. } = &mut **base {
135                if let Some(e) = values.remove(name) {
136                    *expr = e;
137                    return simplify_expression(expr, ga);
138                }
139            }
140            r
141        }
142        Expression::Cast { from, to } => {
143            let can_inline = simplify_expression(from, ga);
144            let new = if from.ty() == *to {
145                Some(std::mem::take(&mut **from))
146            } else {
147                match (&**from, to) {
148                    (Expression::NumberLiteral(x, Unit::None), Type::String) => {
149                        Some(Expression::StringLiteral(x.to_smolstr()))
150                    }
151                    (Expression::Struct { values, .. }, Type::Struct(ty)) => {
152                        Some(Expression::Struct { ty: ty.clone(), values: values.clone() })
153                    }
154                    _ => None,
155                }
156            };
157            if let Some(new) = new {
158                *expr = new;
159            }
160            can_inline
161        }
162        Expression::MinMax { op, lhs, rhs, ty: _ } => {
163            let can_inline = simplify_expression(lhs, ga) & simplify_expression(rhs, ga);
164            if let (Expression::NumberLiteral(lhs, u), Expression::NumberLiteral(rhs, _)) =
165                (&**lhs, &**rhs)
166            {
167                let v = match op {
168                    MinMaxOp::Min => lhs.min(*rhs),
169                    MinMaxOp::Max => lhs.max(*rhs),
170                };
171                *expr = Expression::NumberLiteral(v, *u);
172            }
173            can_inline
174        }
175        Expression::Condition { condition, true_expr, false_expr } => {
176            let mut can_inline = simplify_expression(condition, ga);
177            can_inline &= match &**condition {
178                Expression::BoolLiteral(true) => {
179                    *expr = *true_expr.clone();
180                    simplify_expression(expr, ga)
181                }
182                Expression::BoolLiteral(false) => {
183                    *expr = *false_expr.clone();
184                    simplify_expression(expr, ga)
185                }
186                _ => simplify_expression(true_expr, ga) & simplify_expression(false_expr, ga),
187            };
188            can_inline
189        }
190        // disable this simplification for store local variable, as "let" is not an expression in rust
191        Expression::CodeBlock(stmts)
192            if stmts.len() == 1 && !matches!(stmts[0], Expression::StoreLocalVariable { .. }) =>
193        {
194            *expr = stmts[0].clone();
195            simplify_expression(expr, ga)
196        }
197        Expression::FunctionCall { function, arguments, .. } => {
198            let mut args_can_inline = true;
199            for arg in arguments.iter_mut() {
200                args_can_inline &= simplify_expression(arg, ga);
201            }
202            if args_can_inline {
203                if let Some(inlined) = try_inline_function(function, arguments, ga) {
204                    *expr = inlined;
205                    return true;
206                }
207            }
208            false
209        }
210        Expression::ElementReference { .. } => false,
211        Expression::LayoutCacheAccess { .. } => false,
212        Expression::SolveLayout { .. } => false,
213        Expression::ComputeLayoutInfo { .. } => false,
214        _ => {
215            let mut result = true;
216            expr.visit_mut(|expr| result &= simplify_expression(expr, ga));
217            result
218        }
219    }
220}
221
222/// Will extract the property binding from the given named reference
223/// and propagate constant expression within it. If that's possible,
224/// return the new expression
225fn extract_constant_property_reference(
226    nr: &NamedReference,
227    ga: &GlobalAnalysis,
228) -> Option<Expression> {
229    debug_assert!(nr.is_constant());
230    // find the binding.
231    let mut element = nr.element();
232    let mut expression = loop {
233        if let Some(binding) = element.borrow().bindings.get(nr.name()) {
234            let binding = binding.borrow();
235            if !binding.two_way_bindings.is_empty() {
236                // TODO: In practice, we should still find out what the real binding is
237                // and solve that.
238                return None;
239            }
240            if !matches!(binding.expression, Expression::Invalid) {
241                break binding.expression.clone();
242            }
243        };
244        if let Some(decl) = element.clone().borrow().property_declarations.get(nr.name()) {
245            if let Some(alias) = &decl.is_alias {
246                return extract_constant_property_reference(alias, ga);
247            }
248        } else if let ElementType::Component(c) = &element.clone().borrow().base_type {
249            element = c.root_element.clone();
250            continue;
251        }
252
253        // There is no binding for this property, return the default value
254        let ty = nr.ty();
255        debug_assert!(!matches!(ty, Type::Invalid));
256        return Some(Expression::default_value_for_type(&ty));
257    };
258    if !(simplify_expression(&mut expression, ga)) {
259        return None;
260    }
261    Some(expression)
262}
263
264fn try_inline_function(
265    function: &Callable,
266    arguments: &[Expression],
267    ga: &GlobalAnalysis,
268) -> Option<Expression> {
269    let function = match function {
270        Callable::Function(function) => function,
271        Callable::Builtin(b) => return try_inline_builtin_function(b, arguments, ga),
272        _ => return None,
273    };
274    if !function.is_constant() {
275        return None;
276    }
277    let mut body = extract_constant_property_reference(function, ga)?;
278
279    fn substitute_arguments_recursive(e: &mut Expression, arguments: &[Expression]) {
280        if let Expression::FunctionParameterReference { index, ty } = e {
281            let e_new = arguments.get(*index).expect("reference to invalid arg").clone();
282            debug_assert_eq!(e_new.ty(), *ty);
283            *e = e_new;
284        } else {
285            e.visit_mut(|e| substitute_arguments_recursive(e, arguments));
286        }
287    }
288    substitute_arguments_recursive(&mut body, arguments);
289
290    if simplify_expression(&mut body, ga) {
291        Some(body)
292    } else {
293        None
294    }
295}
296
297fn try_inline_builtin_function(
298    b: &BuiltinFunction,
299    args: &[Expression],
300    ga: &GlobalAnalysis,
301) -> Option<Expression> {
302    let a = |idx: usize| -> Option<f64> {
303        match args.get(idx)? {
304            Expression::NumberLiteral(n, Unit::None) => Some(*n),
305            _ => None,
306        }
307    };
308    let num = |n: f64| Some(Expression::NumberLiteral(n, Unit::None));
309
310    match b {
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!(!e.is_constant(None), "{e:?} should not be constant since some-value can vary at runtime");
419            },
420        },
421        Case {
422            default_font_size: "default-font-size: 25px;",
423            another_window: "export component AnotherWindow inherits Window { default-font-size: 8px; }",
424            check_expression: |e|  {
425                assert!(e.is_constant(None) && !matches!(e, Expression::NumberLiteral(_,_ )), "{e:?} should be constant but not known at compile time since there are two windows");
426            },
427        },
428        Case {
429            default_font_size: "default-font-size: 25px;",
430            another_window: "export component AnotherWindow inherits Window { }",
431            check_expression: |e|  {
432                assert!(!e.is_constant(None), "should not be const since at least one window has it unset");
433            },
434        },
435        Case {
436            default_font_size: "default-font-size: 20px;",
437            another_window: "export component AnotherWindow inherits Window { default-font-size: 20px;  }",
438            check_expression: |e| assert_expr_is_mul(e, 5.0, 20.0)
439        },
440        Case {
441            default_font_size: "default-font-size: 20px;",
442            another_window: "export component AnotherWindow inherits Window { in property <float> f: 1; default-font-size: 20px*f;  }",
443            check_expression: |e| {
444                assert!(!e.is_constant(None), "{e:?} should not be constant since 'f' can vary at runtime");
445            },
446        },
447
448    ] {
449        let source = format!(
450            r#"
451component SomeComponent {{
452    in-out property <length> rem-prop: 5rem;
453}}
454
455{another_window}
456
457export component Foo inherits Window {{
458    in property <length> some-value: 45px;
459    {default_font_size}
460    sc1 := SomeComponent {{}}
461    sc2 := SomeComponent {{}}
462
463    out property <length> test: sc1.rem-prop;
464}}
465"#
466        );
467
468        let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
469
470        let doc_node = crate::parser::parse(
471            source.clone(),
472            Some(std::path::Path::new("HELLO")),
473            &mut test_diags,
474        );
475        let mut compiler_config =
476            crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
477        compiler_config.style = Some("fluent".into());
478        let (doc, diag, _) =
479            spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
480        assert!(!diag.has_errors(), "slint compile error {:#?}", diag.to_string_vec());
481
482        let bindings = &doc.inner_components.last().unwrap().root_element.borrow().bindings;
483        let out1_binding = bindings.get("test").unwrap().borrow().expression.clone();
484        check_expression(&out1_binding);
485    }
486}