Skip to main content

i_slint_compiler/llr/optim_passes/
inline_expressions.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//! Inline properties that are simple enough to be inlined
5//!
6//! If an expression does a single property access or less, it can be inlined
7//! in the calling expression
8
9use crate::expression_tree::{BuiltinFunction, ImageReference};
10use crate::llr::{CompilationUnit, EvaluationContext, Expression};
11
12const PROPERTY_ACCESS_COST: isize = 1000;
13const ALLOC_COST: isize = 700;
14const ARRAY_INDEX_COST: isize = 500;
15/// The threshold from which we consider an expression to be worth inlining.
16/// less than two allocations. (since property access usually cost one allocation)
17const INLINE_THRESHOLD: isize = ALLOC_COST * 2 - 10;
18/// Property that are used only once should almost always be inlined unless it is really expensive to compute and we want to cache the result
19const INLINE_SINGLE_THRESHOLD: isize = ALLOC_COST * 10;
20
21// The cost of an expression.
22fn expression_cost(exp: &Expression, ctx: &EvaluationContext) -> isize {
23    let mut cost = match exp {
24        Expression::StringLiteral(_) => ALLOC_COST,
25        Expression::NumberLiteral(_) => 0,
26        Expression::BoolLiteral(_) => 0,
27        Expression::KeysLiteral(_) => 0,
28        Expression::PropertyReference(_) => PROPERTY_ACCESS_COST,
29        Expression::FunctionParameterReference { .. } => return isize::MAX,
30        Expression::StoreLocalVariable { .. } => 0,
31        Expression::ReadLocalVariable { .. } => 1,
32        Expression::StructFieldAccess { .. } => 1,
33        Expression::ArrayIndex { .. } => ARRAY_INDEX_COST,
34        Expression::Cast { .. } => 0,
35        Expression::CodeBlock(_) => 0,
36        Expression::BuiltinFunctionCall { function, .. } => builtin_function_cost(function),
37        Expression::CallBackCall { callback, .. } => callback_cost(callback, ctx),
38        Expression::FunctionCall { function, .. } => callback_cost(function, ctx),
39        Expression::ItemMemberFunctionCall { function } => callback_cost(function, ctx),
40        Expression::ExtraBuiltinFunctionCall { .. } => return isize::MAX,
41        Expression::PropertyAssignment { .. } => return isize::MAX,
42        Expression::ModelDataAssignment { .. } => return isize::MAX,
43        Expression::ArrayIndexAssignment { .. } => return isize::MAX,
44        Expression::SliceIndexAssignment { .. } => return isize::MAX,
45        Expression::BinaryExpression { .. } => 1,
46        Expression::UnaryOp { .. } => 1,
47        // Avoid inlining calls to load the image from the cache, as in the worst case the image isn't cached
48        // and repeated calls will load the image over and over again. It's better to keep the image cached in the
49        // `property<image>` of the `Image` element, with the exception of embedded textures.
50        Expression::ImageReference {
51            resource_ref: ImageReference::EmbeddedTexture { .. }, ..
52        } => 1,
53        Expression::ImageReference { .. } => return isize::MAX,
54        Expression::Condition { condition, true_expr, false_expr } => {
55            return expression_cost(condition, ctx)
56                .saturating_add(
57                    expression_cost(true_expr, ctx).max(expression_cost(false_expr, ctx)),
58                )
59                .saturating_add(10);
60        }
61        // Never inline an array because it is a model and when shared it needs to keep its identity
62        // (cf #5249)  (otherwise it would be `ALLOC_COST`)
63        Expression::Array { .. } => return isize::MAX,
64        Expression::Struct { .. } => 1,
65        Expression::EasingCurve(_) => 1,
66        Expression::LinearGradient { .. } => ALLOC_COST,
67        Expression::RadialGradient { .. } => ALLOC_COST,
68        Expression::ConicGradient { .. } => ALLOC_COST,
69        Expression::EnumerationValue(_) => 0,
70        Expression::LayoutCacheAccess { .. } => PROPERTY_ACCESS_COST,
71        Expression::GridRepeaterCacheAccess { .. } => PROPERTY_ACCESS_COST,
72        Expression::WithLayoutItemInfo { .. } => return isize::MAX,
73        Expression::WithFlexboxLayoutItemInfo { .. } => return isize::MAX,
74        Expression::SolveFlexboxLayoutWithMeasure { .. } => return isize::MAX,
75        Expression::WithGridInputData { .. } => return isize::MAX,
76        Expression::MinMax { .. } => 10,
77        Expression::EmptyComponentFactory => 10,
78        Expression::EmptyDataTransfer => 10,
79        Expression::TranslationReference { .. } => PROPERTY_ACCESS_COST + 2 * ALLOC_COST,
80    };
81
82    exp.visit(|e| cost = cost.saturating_add(expression_cost(e, ctx)));
83
84    cost
85}
86
87fn callback_cost(_callback: &crate::llr::MemberReference, _ctx: &EvaluationContext) -> isize {
88    // TODO: lookup the callback and find out what it does
89    isize::MAX
90}
91
92fn builtin_function_cost(function: &BuiltinFunction) -> isize {
93    match function {
94        BuiltinFunction::GetWindowScaleFactor => PROPERTY_ACCESS_COST,
95        BuiltinFunction::GetWindowDefaultFontSize => PROPERTY_ACCESS_COST,
96        BuiltinFunction::AnimationTick => PROPERTY_ACCESS_COST,
97        BuiltinFunction::DecimalSeparator => PROPERTY_ACCESS_COST,
98        BuiltinFunction::Debug => isize::MAX,
99        BuiltinFunction::Mod => 10,
100        BuiltinFunction::Round => 10,
101        BuiltinFunction::Ceil => 10,
102        BuiltinFunction::Floor => 10,
103        BuiltinFunction::Abs => 10,
104        BuiltinFunction::Sqrt => 10,
105        BuiltinFunction::Cos => 10,
106        BuiltinFunction::Sin => 10,
107        BuiltinFunction::Tan => 10,
108        BuiltinFunction::ACos => 10,
109        BuiltinFunction::ASin => 10,
110        BuiltinFunction::ATan => 10,
111        BuiltinFunction::ATan2 => 10,
112        BuiltinFunction::Log => 10,
113        BuiltinFunction::Ln => 10,
114        BuiltinFunction::Pow => 10,
115        BuiltinFunction::Exp => 10,
116        BuiltinFunction::ToFixed => ALLOC_COST,
117        BuiltinFunction::ToPrecision => ALLOC_COST,
118        BuiltinFunction::ToStringUnlocalized => ALLOC_COST,
119        BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX,
120        BuiltinFunction::ShowPopupWindow
121        | BuiltinFunction::ClosePopupWindow
122        | BuiltinFunction::ShowPopupMenu
123        | BuiltinFunction::ShowPopupMenuInternal => isize::MAX,
124        BuiltinFunction::SetSelectionOffsets => isize::MAX,
125        BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST,
126        BuiltinFunction::StringToFloat => 50,
127        BuiltinFunction::StringIsFloat => 50,
128        BuiltinFunction::StringIsEmpty => 50,
129        BuiltinFunction::StringCharacterCount => 50,
130        BuiltinFunction::StringToLowercase | BuiltinFunction::StringToUppercase => ALLOC_COST,
131        BuiltinFunction::KeysToString => ALLOC_COST,
132        BuiltinFunction::ColorRgbaStruct => 50,
133        BuiltinFunction::ColorHsvaStruct => 50,
134        BuiltinFunction::ColorOklchStruct => 50,
135        BuiltinFunction::ColorBrighter => 50,
136        BuiltinFunction::ColorDarker => 50,
137        BuiltinFunction::ColorTransparentize => 50,
138        BuiltinFunction::ColorMix => 50,
139        BuiltinFunction::ColorWithAlpha => 50,
140        BuiltinFunction::ImageSize => 50,
141        BuiltinFunction::ArrayLength => 50,
142        BuiltinFunction::Rgb => 50,
143        BuiltinFunction::Hsv => 50,
144        BuiltinFunction::Oklch => 50,
145        BuiltinFunction::ImplicitLayoutInfo(_) => isize::MAX,
146        BuiltinFunction::ItemAbsolutePosition => isize::MAX,
147        BuiltinFunction::RegisterCustomFontByPath => isize::MAX,
148        BuiltinFunction::RegisterCustomFontByMemory => isize::MAX,
149        BuiltinFunction::RegisterBitmapFont => isize::MAX,
150        BuiltinFunction::ColorScheme => PROPERTY_ACCESS_COST,
151        BuiltinFunction::AccentColor => PROPERTY_ACCESS_COST,
152        BuiltinFunction::SupportsNativeMenuBar => 10,
153        BuiltinFunction::SetupMenuBar => isize::MAX,
154        BuiltinFunction::SetupSystemTrayIcon => isize::MAX,
155        BuiltinFunction::MonthDayCount => isize::MAX,
156        BuiltinFunction::MonthOffset => isize::MAX,
157        BuiltinFunction::FormatDate => isize::MAX,
158        BuiltinFunction::DateNow => isize::MAX,
159        BuiltinFunction::ValidDate => isize::MAX,
160        BuiltinFunction::ParseDate => isize::MAX,
161        BuiltinFunction::SetTextInputFocused => PROPERTY_ACCESS_COST,
162        BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST,
163        BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
164        BuiltinFunction::Use24HourFormat => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
165        BuiltinFunction::UpdateTimers => 10,
166        BuiltinFunction::DetectOperatingSystem => 10,
167        BuiltinFunction::StartTimer => 10,
168        BuiltinFunction::StopTimer => 10,
169        BuiltinFunction::RestartTimer => 10,
170        BuiltinFunction::ParseMarkdown => isize::MAX,
171        BuiltinFunction::StringToStyledText => ALLOC_COST,
172        BuiltinFunction::ColorToStyledText => ALLOC_COST,
173        BuiltinFunction::OpenUrl => isize::MAX,
174        BuiltinFunction::MacosBringAllWindowsToFront => isize::MAX,
175    }
176}
177
178pub fn inline_simple_expressions(root: &CompilationUnit) {
179    root.for_each_expression(&mut |e, ctx| {
180        inline_simple_expressions_in_expression(&mut e.borrow_mut(), ctx)
181    })
182}
183
184fn inline_simple_expressions_in_expression(expr: &mut Expression, ctx: &EvaluationContext) {
185    if let Expression::PropertyReference(prop) = expr {
186        let prop_info = ctx.property_info(prop);
187        if prop_info.analysis.as_ref().is_some_and(|a| !a.is_set && !a.is_set_externally) {
188            if let Some((binding, map)) = prop_info.binding {
189                if binding.animation.is_none()
190                    // State info binding are special and the binding cannot be inlined or used.
191                    && !binding.is_state_info
192                {
193                    let mapped_ctx = map.map_context(ctx);
194                    let cost = expression_cost(&binding.expression.borrow(), &mapped_ctx);
195                    let use_count = binding.use_count.get();
196                    debug_assert!(
197                        use_count > 0,
198                        "We use a property and its count is zero: {}",
199                        crate::llr::pretty_print::DisplayPropertyRef(prop, ctx)
200                    );
201                    if cost <= INLINE_THRESHOLD
202                        || (use_count == 1 && cost <= INLINE_SINGLE_THRESHOLD)
203                    {
204                        // Perform inlining
205                        *expr = binding.expression.borrow().clone();
206                        map.map_expression(expr);
207                        // adjust use count
208                        binding.use_count.set(use_count - 1);
209                        if let Some(use_count) = prop_info.use_count {
210                            use_count.set(use_count.get() - 1);
211                        }
212                        adjust_use_count(expr, ctx, 1);
213                        if use_count == 1 {
214                            adjust_use_count(&binding.expression.borrow(), &mapped_ctx, -1);
215                            binding.expression.replace(Expression::CodeBlock(Vec::new()));
216                        }
217                    }
218                }
219            } else if let Some(use_count) = prop_info.use_count
220                && let Some(e) = Expression::default_value_for_type(&prop_info.ty)
221            {
222                use_count.set(use_count.get() - 1);
223                *expr = e;
224            }
225        }
226    };
227
228    expr.visit_mut(|e| inline_simple_expressions_in_expression(e, ctx));
229}
230
231fn adjust_use_count(expr: &Expression, ctx: &EvaluationContext, adjust: isize) {
232    expr.visit_property_references(ctx, &mut |p, ctx| {
233        let prop_info = ctx.property_info(p);
234        if let Some(use_count) = prop_info.use_count {
235            use_count.set(use_count.get().checked_add_signed(adjust).unwrap());
236        }
237        if let Some((binding, _)) = prop_info.binding {
238            let use_count = binding.use_count.get().checked_add_signed(adjust).unwrap();
239            binding.use_count.set(use_count);
240        }
241    });
242}