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::langtype::Type;
11use crate::llr::{CompilationUnit, ContextMap, EvaluationContext, Expression};
12
13const PROPERTY_ACCESS_COST: isize = 1000;
14const ALLOC_COST: isize = 700;
15const ARRAY_INDEX_COST: isize = 500;
16/// The threshold from which we consider an expression to be worth inlining.
17/// less than two allocations. (since property access usually cost one allocation)
18const INLINE_THRESHOLD: isize = ALLOC_COST * 2 - 10;
19/// 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
20const INLINE_SINGLE_THRESHOLD: isize = ALLOC_COST * 10;
21
22// The cost of an expression.
23fn expression_cost(exp: &Expression, ctx: &EvaluationContext) -> isize {
24    let mut cost = match exp {
25        Expression::StringLiteral(_) => ALLOC_COST,
26        Expression::NumberLiteral(_) => 0,
27        Expression::BoolLiteral(_) => 0,
28        Expression::KeysLiteral(_) => 0,
29        Expression::PropertyReference(_) => PROPERTY_ACCESS_COST,
30        Expression::FunctionParameterReference { .. } => return isize::MAX,
31        Expression::StoreLocalVariable { .. } => 0,
32        Expression::ReadLocalVariable { .. } => 1,
33        Expression::StructFieldAccess { .. } => 1,
34        Expression::ArrayIndex { .. } => ARRAY_INDEX_COST,
35        Expression::Cast { .. } => 0,
36        Expression::CodeBlock(_) => 0,
37        Expression::BuiltinFunctionCall { function, .. } => builtin_function_cost(function),
38        Expression::CallBackCall { callback, .. } => callback_cost(callback, ctx),
39        Expression::FunctionCall { function, .. } => callback_cost(function, ctx),
40        Expression::ItemMemberFunctionCall { function } => callback_cost(function, ctx),
41        Expression::ExtraBuiltinFunctionCall { .. } => return isize::MAX,
42        Expression::PropertyAssignment { .. } => return isize::MAX,
43        Expression::ModelDataAssignment { .. } => return isize::MAX,
44        Expression::ArrayIndexAssignment { .. } => return isize::MAX,
45        Expression::SliceIndexAssignment { .. } => return isize::MAX,
46        Expression::BinaryExpression { .. } => 1,
47        Expression::UnaryOp { .. } => 1,
48        // Avoid inlining calls to load the image from the cache, as in the worst case the image isn't cached
49        // and repeated calls will load the image over and over again. It's better to keep the image cached in the
50        // `property<image>` of the `Image` element, with the exception of embedded textures.
51        Expression::ImageReference {
52            resource_ref: ImageReference::EmbeddedTexture { .. }, ..
53        } => 1,
54        Expression::ImageReference { .. } => return isize::MAX,
55        Expression::Condition { condition, true_expr, false_expr } => {
56            return expression_cost(condition, ctx)
57                .saturating_add(
58                    expression_cost(true_expr, ctx).max(expression_cost(false_expr, ctx)),
59                )
60                .saturating_add(10);
61        }
62        // Never inline an array because it is a model and when shared it needs to keep its identity
63        // (cf #5249)  (otherwise it would be `ALLOC_COST`)
64        Expression::Array { .. } => return isize::MAX,
65        Expression::Struct { .. } => 1,
66        Expression::EasingCurve(_) => 1,
67        Expression::MouseCursor(_) => 1,
68        Expression::LinearGradient { .. } => ALLOC_COST,
69        Expression::RadialGradient { .. } => ALLOC_COST,
70        Expression::ConicGradient { .. } => ALLOC_COST,
71        Expression::EnumerationValue(_) => 0,
72        Expression::LayoutCacheAccess { .. } => PROPERTY_ACCESS_COST,
73        Expression::GridRepeaterCacheAccess { .. } => PROPERTY_ACCESS_COST,
74        Expression::WithLayoutItemInfo { .. } => return isize::MAX,
75        Expression::WithFlexboxLayoutItemInfo { .. } => return isize::MAX,
76        Expression::SolveFlexboxLayoutWithMeasure { .. } => return isize::MAX,
77        Expression::BoxLayoutInfoOrthoWithMeasure { .. } => return isize::MAX,
78        Expression::FlexboxLayoutInfoCrossAxisWithMeasure { .. } => return isize::MAX,
79        Expression::WithGridInputData { .. } => return isize::MAX,
80        Expression::MinMax { .. } => 10,
81        Expression::EmptyComponentFactory => 10,
82        Expression::EmptyDataTransfer => 10,
83        Expression::TranslationReference { .. } => PROPERTY_ACCESS_COST + 2 * ALLOC_COST,
84        // The body cost is added by the visit() walk below; returning the body
85        // cost here would double-count it.
86        Expression::Closure { .. } => 0,
87        // Don't inline: that could duplicate or relocate the hook.
88        Expression::DebugHook { .. } => return isize::MAX,
89    };
90
91    exp.visit(|e| cost = cost.saturating_add(expression_cost(e, ctx)));
92
93    cost
94}
95
96fn callback_cost(_callback: &crate::llr::MemberReference, _ctx: &EvaluationContext) -> isize {
97    // TODO: lookup the callback and find out what it does
98    isize::MAX
99}
100
101fn builtin_function_cost(function: &BuiltinFunction) -> isize {
102    match function {
103        BuiltinFunction::GetWindowScaleFactor => PROPERTY_ACCESS_COST,
104        BuiltinFunction::GetWindowDefaultFontSize => PROPERTY_ACCESS_COST,
105        BuiltinFunction::AnimationTick => PROPERTY_ACCESS_COST,
106        BuiltinFunction::DecimalSeparator => PROPERTY_ACCESS_COST,
107        BuiltinFunction::DefaultWindowTitle => PROPERTY_ACCESS_COST,
108        BuiltinFunction::Debug => isize::MAX,
109        BuiltinFunction::Mod => 10,
110        BuiltinFunction::Round => 10,
111        BuiltinFunction::Ceil => 10,
112        BuiltinFunction::Floor => 10,
113        BuiltinFunction::Abs => 10,
114        BuiltinFunction::Sqrt => 10,
115        BuiltinFunction::Cos => 10,
116        BuiltinFunction::Sin => 10,
117        BuiltinFunction::Tan => 10,
118        BuiltinFunction::ACos => 10,
119        BuiltinFunction::ASin => 10,
120        BuiltinFunction::ATan => 10,
121        BuiltinFunction::ATan2 => 10,
122        BuiltinFunction::Log => 10,
123        BuiltinFunction::Ln => 10,
124        BuiltinFunction::Pow => 10,
125        BuiltinFunction::Exp => 10,
126        BuiltinFunction::ToFixed => ALLOC_COST,
127        BuiltinFunction::ToPrecision => ALLOC_COST,
128        BuiltinFunction::ToStringUnlocalized => ALLOC_COST,
129        BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX,
130        BuiltinFunction::ShowPopupWindow
131        | BuiltinFunction::ClosePopupWindow
132        | BuiltinFunction::ShowPopupMenu
133        | BuiltinFunction::ShowPopupMenuInternal => isize::MAX,
134        BuiltinFunction::SetSelectionOffsets => isize::MAX,
135        BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST,
136        BuiltinFunction::StringToFloat => 50,
137        BuiltinFunction::StringIsFloat => 50,
138        BuiltinFunction::StringIsEmpty => 50,
139        BuiltinFunction::StringCharacterCount => 50,
140        BuiltinFunction::StringStartsWith | BuiltinFunction::StringEndsWith => 50,
141        BuiltinFunction::StringToLowercase | BuiltinFunction::StringToUppercase => ALLOC_COST,
142        BuiltinFunction::StringReplaceAll => ALLOC_COST,
143        BuiltinFunction::KeysToString => ALLOC_COST,
144        BuiltinFunction::ColorRgbaStruct => 50,
145        BuiltinFunction::ColorHsvaStruct => 50,
146        BuiltinFunction::ColorOklchStruct => 50,
147        BuiltinFunction::ColorBrighter => 50,
148        BuiltinFunction::ColorDarker => 50,
149        BuiltinFunction::ColorTransparentize => 50,
150        BuiltinFunction::ColorMix => 50,
151        BuiltinFunction::ColorWithAlpha => 50,
152        BuiltinFunction::ImageSize => 50,
153        BuiltinFunction::ArrayLength => 50,
154        BuiltinFunction::ArrayPush
155        | BuiltinFunction::ArrayRemove
156        | BuiltinFunction::ArrayInsert => ALLOC_COST,
157        BuiltinFunction::Rgb => 50,
158        BuiltinFunction::Hsv => 50,
159        BuiltinFunction::Oklch => 50,
160        BuiltinFunction::ImplicitLayoutInfo(_) => isize::MAX,
161        BuiltinFunction::ItemAbsolutePosition => isize::MAX,
162        BuiltinFunction::RegisterCustomFontByPath => isize::MAX,
163        BuiltinFunction::RegisterCustomFontByMemory => isize::MAX,
164        BuiltinFunction::RegisterBitmapFont => isize::MAX,
165        BuiltinFunction::ColorScheme => PROPERTY_ACCESS_COST,
166        BuiltinFunction::AccentColor => PROPERTY_ACCESS_COST,
167        BuiltinFunction::SupportsNativeMenuBar => 10,
168        BuiltinFunction::SetupMenuBar => isize::MAX,
169        BuiltinFunction::SetupSystemTrayIcon => isize::MAX,
170        BuiltinFunction::MonthDayCount => isize::MAX,
171        BuiltinFunction::MonthOffset => isize::MAX,
172        BuiltinFunction::FormatDate => isize::MAX,
173        BuiltinFunction::DateNow => isize::MAX,
174        BuiltinFunction::ValidDate => isize::MAX,
175        BuiltinFunction::ParseDate => isize::MAX,
176        BuiltinFunction::SetTextInputFocused => PROPERTY_ACCESS_COST,
177        BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST,
178        BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
179        BuiltinFunction::Use24HourFormat => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
180        BuiltinFunction::UpdateTimers => 10,
181        BuiltinFunction::DetectOperatingSystem => 10,
182        BuiltinFunction::StartTimer => 10,
183        BuiltinFunction::StopTimer => 10,
184        BuiltinFunction::RestartTimer => 10,
185        BuiltinFunction::ParseMarkdown => isize::MAX,
186        BuiltinFunction::StringToStyledText => ALLOC_COST,
187        BuiltinFunction::ColorToStyledText => ALLOC_COST,
188        BuiltinFunction::OpenUrl => isize::MAX,
189        BuiltinFunction::MacosBringAllWindowsToFront => isize::MAX,
190        BuiltinFunction::PathPointAt => isize::MAX,
191        BuiltinFunction::PathAngleAt => isize::MAX,
192        // Iterating the model and running the closure is unbounded; never inline.
193        BuiltinFunction::ArrayAny | BuiltinFunction::ArrayAll | BuiltinFunction::ArrayFindIndex => {
194            isize::MAX
195        }
196    }
197}
198
199pub fn inline_simple_expressions(root: &CompilationUnit) {
200    // Counter to give each inlined function's argument locals a unique name.
201    let mut counter = 0usize;
202    root.for_each_expression(&mut |e, ctx| {
203        inline_simple_expressions_in_expression(&mut e.borrow_mut(), ctx, &mut counter)
204    })
205}
206
207fn inline_simple_expressions_in_expression(
208    expr: &mut Expression,
209    ctx: &EvaluationContext,
210    counter: &mut usize,
211) {
212    // Inline a call to a function that is called exactly once: move its body to the call site.
213    if let Expression::FunctionCall { function, .. } = expr {
214        let inline_target = ctx.function_info(function).and_then(|(f, map)| {
215            if f.use_count.get() != 1 || !body_is_inline_safe(&f.code.borrow(), &map) {
216                return None;
217            }
218            f.use_count.set(0);
219            // count_property_use counts the body in the function's context; re-home the use
220            // counts to the call site, where a reference can resolve to a parent-set binding
221            // (e.g. `height: 100%`) that is invisible in the function and so under-counted.
222            adjust_use_count(&f.code.borrow(), &map.map_context(ctx), -1);
223            // Take the body out so it isn't also inlined in place when
224            // `for_each_expression` reaches it, which would double-count uses.
225            let body = f.code.replace(Expression::CodeBlock(Vec::new()));
226            Some((body, f.args.clone(), map))
227        });
228        if let Some((mut body, arg_types, map)) = inline_target {
229            let Expression::FunctionCall { arguments, .. } =
230                std::mem::replace(expr, Expression::CodeBlock(Vec::new()))
231            else {
232                unreachable!()
233            };
234            let uid = *counter;
235            *counter += 1;
236            map.map_expression(&mut body);
237            // Re-count in the call-site context (see above).
238            adjust_use_count(&body, ctx, 1);
239            substitute_function_parameters(&mut body, uid, &arg_types);
240            *expr = if arguments.is_empty() {
241                body
242            } else {
243                let mut stmts = arguments
244                    .into_iter()
245                    .enumerate()
246                    .map(|(i, a)| Expression::StoreLocalVariable {
247                        name: function_arg_local_name(uid, i),
248                        value: Box::new(a),
249                    })
250                    .collect::<Vec<_>>();
251                stmts.push(body);
252                Expression::CodeBlock(stmts)
253            };
254            // Inline further within the freshly inlined body (nested single calls,
255            // constant properties now visible at the call site, ...).
256            inline_simple_expressions_in_expression(expr, ctx, counter);
257            return;
258        }
259    }
260
261    if let Expression::PropertyReference(prop) = expr {
262        let prop_info = ctx.property_info(prop);
263        if prop_info.analysis.as_ref().is_some_and(|a| !a.is_set && !a.is_set_externally) {
264            if let Some((binding, map)) = prop_info.binding {
265                if binding.animation.is_none() && binding.kind != super::super::BindingKind::State {
266                    let mapped_ctx = map.map_context(ctx);
267                    let cost = expression_cost(&binding.expression.borrow(), &mapped_ctx);
268                    let use_count = binding.use_count.get();
269                    debug_assert!(
270                        use_count > 0,
271                        "We use a property and its count is zero: {}",
272                        crate::llr::pretty_print::DisplayPropertyRef(prop, ctx)
273                    );
274                    if cost <= INLINE_THRESHOLD
275                        || (use_count == 1 && cost <= INLINE_SINGLE_THRESHOLD)
276                    {
277                        // Perform inlining
278                        *expr = binding.expression.borrow().clone();
279                        map.map_expression(expr);
280                        // adjust use count
281                        binding.use_count.set(use_count - 1);
282                        if let Some(use_count) = prop_info.use_count {
283                            use_count.set(use_count.get() - 1);
284                        }
285                        adjust_use_count(expr, ctx, 1);
286                        if use_count == 1 {
287                            adjust_use_count(&binding.expression.borrow(), &mapped_ctx, -1);
288                            binding.expression.replace(Expression::CodeBlock(Vec::new()));
289                        }
290                    }
291                }
292            } else if let Some(use_count) = prop_info.use_count
293                && let Some(e) = Expression::default_value_for_type(&prop_info.ty)
294            {
295                use_count.set(use_count.get() - 1);
296                *expr = e;
297            }
298        }
299    };
300
301    expr.visit_mut(|e| inline_simple_expressions_in_expression(e, ctx, counter));
302}
303
304/// Whether a function body can be moved to its single call site through `map`.
305///
306/// Unsafe when it references state that only exists in the declaring component
307/// and cannot be remapped by `ContextMap::map_expression`:
308/// the menu item tree of a popup menu, and `UpdateTimers`,
309/// refer to the enclosing component implicitly,
310/// so they can only move within the same component.
311/// Also unsafe when it reads a parameter more than once:
312/// a real call clones each read (`args.N.clone()`),
313/// but the inlined body reads a local that a second read would move.
314fn body_is_inline_safe(exp: &Expression, map: &ContextMap) -> bool {
315    let mut params = std::collections::HashSet::new();
316    let mut safe = true;
317    exp.visit_recursive(&mut |e| match e {
318        Expression::FunctionParameterReference { index } => safe &= params.insert(*index),
319        Expression::BuiltinFunctionCall { function, .. } => {
320            safe &= match function {
321                BuiltinFunction::ShowPopupMenu
322                | BuiltinFunction::ShowPopupMenuInternal
323                | BuiltinFunction::UpdateTimers => matches!(map, ContextMap::Identity),
324                _ => true,
325            }
326        }
327        _ => {}
328    });
329    safe
330}
331
332/// The local variable name holding argument `index` of the function inlined with `uid`.
333fn function_arg_local_name(uid: usize, index: usize) -> smol_str::SmolStr {
334    smol_str::format_smolstr!("inlined_fn_arg_{uid}_{index}")
335}
336
337/// Replace `FunctionParameterReference` with a read of the local that holds the argument.
338fn substitute_function_parameters(expr: &mut Expression, uid: usize, arg_types: &[Type]) {
339    expr.visit_recursive_mut(&mut |e| {
340        if let Expression::FunctionParameterReference { index } = e {
341            let index = *index;
342            *e = Expression::ReadLocalVariable {
343                name: function_arg_local_name(uid, index),
344                ty: arg_types[index].clone(),
345            };
346        }
347    });
348}
349
350fn adjust_use_count(expr: &Expression, ctx: &EvaluationContext, adjust: isize) {
351    expr.visit_property_references(ctx, &mut |p, ctx| {
352        let prop_info = ctx.property_info(p);
353        if let Some(use_count) = prop_info.use_count {
354            use_count.set(use_count.get().checked_add_signed(adjust).unwrap());
355        }
356        if let Some((binding, _)) = prop_info.binding {
357            let use_count = binding.use_count.get().checked_add_signed(adjust).unwrap();
358            binding.use_count.set(use_count);
359        }
360    });
361}