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::PropertyReference(_) => PROPERTY_ACCESS_COST,
28 Expression::FunctionParameterReference { .. } => return isize::MAX,
29 Expression::StoreLocalVariable { .. } => 0,
30 Expression::ReadLocalVariable { .. } => 1,
31 Expression::StructFieldAccess { .. } => 1,
32 Expression::ArrayIndex { .. } => ARRAY_INDEX_COST,
33 Expression::Cast { .. } => 0,
34 Expression::CodeBlock(_) => 0,
35 Expression::BuiltinFunctionCall { function, .. } => builtin_function_cost(function),
36 Expression::CallBackCall { callback, .. } => callback_cost(callback, ctx),
37 Expression::FunctionCall { function, .. } => callback_cost(function, ctx),
38 Expression::ItemMemberFunctionCall { function } => callback_cost(function, ctx),
39 Expression::ExtraBuiltinFunctionCall { .. } => return isize::MAX,
40 Expression::PropertyAssignment { .. } => return isize::MAX,
41 Expression::ModelDataAssignment { .. } => return isize::MAX,
42 Expression::ArrayIndexAssignment { .. } => return isize::MAX,
43 Expression::BinaryExpression { .. } => 1,
44 Expression::UnaryOp { .. } => 1,
45 // Avoid inlining calls to load the image from the cache, as in the worst case the image isn't cached
46 // and repeated calls will load the image over and over again. It's better to keep the image cached in the
47 // `property<image>` of the `Image` element, with the exception of embedded textures.
48 Expression::ImageReference {
49 resource_ref: ImageReference::EmbeddedTexture { .. }, ..
50 } => 1,
51 Expression::ImageReference { .. } => return isize::MAX,
52 Expression::Condition { condition, true_expr, false_expr } => {
53 return expression_cost(condition, ctx)
54 .saturating_add(
55 expression_cost(true_expr, ctx).max(expression_cost(false_expr, ctx)),
56 )
57 .saturating_add(10);
58 }
59 // Never inline an array because it is a model and when shared it needs to keep its identity
60 // (cf #5249) (otherwise it would be `ALLOC_COST`)
61 Expression::Array { .. } => return isize::MAX,
62 Expression::Struct { .. } => 1,
63 Expression::EasingCurve(_) => 1,
64 Expression::LinearGradient { .. } => ALLOC_COST,
65 Expression::RadialGradient { .. } => ALLOC_COST,
66 Expression::EnumerationValue(_) => 0,
67 Expression::LayoutCacheAccess { .. } => PROPERTY_ACCESS_COST,
68 Expression::BoxLayoutFunction { .. } => return isize::MAX,
69 Expression::ComputeDialogLayoutCells { .. } => return isize::MAX,
70 Expression::MinMax { .. } => 10,
71 Expression::EmptyComponentFactory => 10,
72 Expression::TranslationReference { .. } => PROPERTY_ACCESS_COST + 2 * ALLOC_COST,
73 };
74
75 exp.visit(|e| cost = cost.saturating_add(expression_cost(e, ctx)));
76
77 cost
78}
79
80fn callback_cost(_callback: &crate::llr::PropertyReference, _ctx: &EvaluationContext) -> isize {
81 // TODO: lookup the callback and find out what it does
82 isize::MAX
83}
84
85fn builtin_function_cost(function: &BuiltinFunction) -> isize {
86 match function {
87 BuiltinFunction::GetWindowScaleFactor => PROPERTY_ACCESS_COST,
88 BuiltinFunction::GetWindowDefaultFontSize => PROPERTY_ACCESS_COST,
89 BuiltinFunction::AnimationTick => PROPERTY_ACCESS_COST,
90 BuiltinFunction::Debug => isize::MAX,
91 BuiltinFunction::Mod => 10,
92 BuiltinFunction::Round => 10,
93 BuiltinFunction::Ceil => 10,
94 BuiltinFunction::Floor => 10,
95 BuiltinFunction::Abs => 10,
96 BuiltinFunction::Sqrt => 10,
97 BuiltinFunction::Cos => 10,
98 BuiltinFunction::Sin => 10,
99 BuiltinFunction::Tan => 10,
100 BuiltinFunction::ACos => 10,
101 BuiltinFunction::ASin => 10,
102 BuiltinFunction::ATan => 10,
103 BuiltinFunction::ATan2 => 10,
104 BuiltinFunction::Log => 10,
105 BuiltinFunction::Pow => 10,
106 BuiltinFunction::ToFixed => ALLOC_COST,
107 BuiltinFunction::ToPrecision => ALLOC_COST,
108 BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX,
109 BuiltinFunction::ShowPopupWindow
110 | BuiltinFunction::ClosePopupWindow
111 | BuiltinFunction::ShowPopupMenu => isize::MAX,
112 BuiltinFunction::SetSelectionOffsets => isize::MAX,
113 BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST,
114 BuiltinFunction::StringToFloat => 50,
115 BuiltinFunction::StringIsFloat => 50,
116 BuiltinFunction::StringIsEmpty => 50,
117 BuiltinFunction::StringCharacterCount => 50,
118 BuiltinFunction::StringToLowercase => ALLOC_COST,
119 BuiltinFunction::StringToUppercase => ALLOC_COST,
120 BuiltinFunction::ColorRgbaStruct => 50,
121 BuiltinFunction::ColorHsvaStruct => 50,
122 BuiltinFunction::ColorBrighter => 50,
123 BuiltinFunction::ColorDarker => 50,
124 BuiltinFunction::ColorTransparentize => 50,
125 BuiltinFunction::ColorMix => 50,
126 BuiltinFunction::ColorWithAlpha => 50,
127 BuiltinFunction::ImageSize => 50,
128 BuiltinFunction::ArrayLength => 50,
129 BuiltinFunction::Rgb => 50,
130 BuiltinFunction::Hsv => 50,
131 BuiltinFunction::ImplicitLayoutInfo(_) => isize::MAX,
132 BuiltinFunction::ItemAbsolutePosition => isize::MAX,
133 BuiltinFunction::RegisterCustomFontByPath => isize::MAX,
134 BuiltinFunction::RegisterCustomFontByMemory => isize::MAX,
135 BuiltinFunction::RegisterBitmapFont => isize::MAX,
136 BuiltinFunction::ColorScheme => PROPERTY_ACCESS_COST,
137 BuiltinFunction::SupportsNativeMenuBar => 10,
138 BuiltinFunction::SetupNativeMenuBar => isize::MAX,
139 BuiltinFunction::MonthDayCount => isize::MAX,
140 BuiltinFunction::MonthOffset => isize::MAX,
141 BuiltinFunction::FormatDate => isize::MAX,
142 BuiltinFunction::DateNow => isize::MAX,
143 BuiltinFunction::ValidDate => isize::MAX,
144 BuiltinFunction::ParseDate => isize::MAX,
145 BuiltinFunction::SetTextInputFocused => PROPERTY_ACCESS_COST,
146 BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST,
147 BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
148 BuiltinFunction::Use24HourFormat => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
149 BuiltinFunction::UpdateTimers => 10,
150 }
151}
152
153pub fn inline_simple_expressions(root: &CompilationUnit) {
154 root.for_each_expression(&mut |e: &MutExpression, ctx: &EvaluationContext<'_>| {
155 inline_simple_expressions_in_expression(&mut e.borrow_mut(), ctx)
156 })
157}
158
159fn inline_simple_expressions_in_expression(expr: &mut Expression, ctx: &EvaluationContext) {
160 if let Expression::PropertyReference(prop) = expr {
161 let prop_info = ctx.property_info(prop);
162 if prop_info.analysis.as_ref().is_some_and(|a| !a.is_set && !a.is_set_externally) {
163 if let Some((binding, map)) = prop_info.binding {
164 if binding.animation.is_none()
165 // State info binding are special and the binding cannot be inlined or used.
166 && !binding.is_state_info
167 {
168 let mapped_ctx = map.map_context(ctx);
169 let cost = expression_cost(&binding.expression.borrow(), &mapped_ctx);
170 let use_count = binding.use_count.get();
171 debug_assert!(
172 use_count > 0,
173 "We use a property and its count is zero: {}",
174 crate::llr::pretty_print::DisplayPropertyRef(prop, ctx)
175 );
176 if cost <= INLINE_THRESHOLD
177 || (use_count == 1 && cost <= INLINE_SINGLE_THRESHOLD)
178 {
179 // Perform inlining
180 *expr = binding.expression.borrow().clone();
181 map.map_expression(expr);
182 // adjust use count
183 binding.use_count.set(use_count - 1);
184 if let Some(prop_decl) = prop_info.property_decl {
185 prop_decl.use_count.set(prop_decl.use_count.get() - 1);
186 }
187 adjust_use_count(expr, ctx, 1);
188 if use_count == 1 {
189 adjust_use_count(&binding.expression.borrow(), &mapped_ctx, -1);
190 binding.expression.replace(Expression::CodeBlock(vec![]));
191 }
192 }
193 }
194 } else if let Some(prop_decl) = prop_info.property_decl {
195 if let Some(e) = Expression::default_value_for_type(&prop_decl.ty) {
196 prop_decl.use_count.set(prop_decl.use_count.get() - 1);
197 *expr = e;
198 }
199 }
200 }
201 };
202
203 expr.visit_mut(|e| inline_simple_expressions_in_expression(e, ctx));
204}
205
206fn adjust_use_count(expr: &Expression, ctx: &EvaluationContext, adjust: isize) {
207 expr.visit_property_references(ctx, &mut |p: &PropertyReference, ctx: &EvaluationContext<'_>| {
208 let prop_info: PropertyInfoResult<'_> = ctx.property_info(prop:p);
209 if let Some(property_decl: &Property) = prop_info.property_decl {
210 property_decl
211 .use_count
212 .set(val:property_decl.use_count.get().checked_add_signed(adjust).unwrap());
213 }
214 if let Some((binding: &BindingExpression, _)) = prop_info.binding {
215 let use_count: usize = binding.use_count.get().checked_add_signed(adjust).unwrap();
216 binding.use_count.set(val:use_count);
217 }
218 });
219}
220