Skip to main content

i_slint_compiler/
builtin_macros.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//! This module contains the implementation of the builtin macros.
5//! They are just transformations that convert into some more complicated expression tree
6
7use crate::diagnostics::{BuildDiagnostics, Spanned};
8use crate::expression_tree::{
9    BuiltinFunction, BuiltinMacroFunction, Callable, EasingCurve, Expression, MinMaxOp,
10    MouseCursorInner, Unit,
11};
12use crate::langtype::Type;
13use crate::parser::NodeOrToken;
14use crate::symbol_counters::SymbolCounters;
15use smol_str::{ToSmolStr, format_smolstr};
16
17/// "Expand" the macro `mac` (at location `n`) with the arguments `sub_expr`
18pub fn lower_macro(
19    mac: BuiltinMacroFunction,
20    n: &dyn Spanned,
21    mut sub_expr: impl Iterator<Item = (Expression, Option<NodeOrToken>)>,
22    diag: &mut BuildDiagnostics,
23    symbol_counters: &SymbolCounters,
24) -> Expression {
25    match mac {
26        BuiltinMacroFunction::Min => {
27            min_max_macro(n, MinMaxOp::Min, sub_expr.collect(), diag, symbol_counters)
28        }
29        BuiltinMacroFunction::Max => {
30            min_max_macro(n, MinMaxOp::Max, sub_expr.collect(), diag, symbol_counters)
31        }
32        BuiltinMacroFunction::Clamp => clamp_macro(n, sub_expr.collect(), diag, symbol_counters),
33        BuiltinMacroFunction::Mod => mod_macro(n, sub_expr.collect(), diag, symbol_counters),
34        BuiltinMacroFunction::Abs => abs_macro(n, sub_expr.collect(), diag, symbol_counters),
35        BuiltinMacroFunction::Sign => {
36            let Some((x, arg_node)) = sub_expr.next() else {
37                diag.push_error("Expected one argument".into(), n);
38                return Expression::Invalid;
39            };
40            if sub_expr.next().is_some() {
41                diag.push_error("Expected only one argument".into(), n);
42            }
43            Expression::Condition {
44                condition: Expression::BinaryExpression {
45                    source_location: None,
46                    lhs: x.maybe_convert_to(Type::Float32, &arg_node, diag, symbol_counters).into(),
47                    rhs: Expression::NumberLiteral(0., Unit::None).into(),
48                    op: '<',
49                }
50                .into(),
51                true_expr: Expression::NumberLiteral(-1., Unit::None).into(),
52                false_expr: Expression::NumberLiteral(1., Unit::None).into(),
53                source_location: None,
54            }
55        }
56        BuiltinMacroFunction::Debug => debug_macro(n, sub_expr.collect(), diag, symbol_counters),
57        BuiltinMacroFunction::CubicBezier => {
58            let mut has_error = None;
59            let expected_argument_type_error =
60                "Arguments to cubic bezier curve must be number literal";
61            // FIXME: this is not pretty to be handling there.
62            // Maybe "cubic_bezier" should be a function that is lowered later
63            let mut a = || match sub_expr.next() {
64                None => {
65                    has_error.get_or_insert((n.to_source_location(), "Not enough arguments"));
66                    0.
67                }
68                Some((Expression::NumberLiteral(val, Unit::None), _)) => val as f32,
69                // handle negative numbers
70                Some((Expression::UnaryOp { sub, op: '-' }, n)) => match *sub {
71                    Expression::NumberLiteral(val, Unit::None) => -val as f32,
72                    _ => {
73                        has_error
74                            .get_or_insert((n.to_source_location(), expected_argument_type_error));
75                        0.
76                    }
77                },
78                Some((_, n)) => {
79                    has_error.get_or_insert((n.to_source_location(), expected_argument_type_error));
80                    0.
81                }
82            };
83            let expr = Expression::EasingCurve(EasingCurve::CubicBezier(a(), a(), a(), a()));
84            if let Some((_, n)) = sub_expr.next() {
85                has_error
86                    .get_or_insert((n.to_source_location(), "Too many argument for bezier curve"));
87            }
88            if let Some((n, msg)) = has_error {
89                diag.push_error(msg.into(), &n);
90            }
91
92            expr
93        }
94        BuiltinMacroFunction::Rgb => rgb_macro(n, sub_expr.collect(), diag, symbol_counters),
95        BuiltinMacroFunction::Hsv => hsv_macro(n, sub_expr.collect(), diag, symbol_counters),
96        BuiltinMacroFunction::Oklch => oklch_macro(n, sub_expr.collect(), diag, symbol_counters),
97        BuiltinMacroFunction::ArrayPush => {
98            array_push_macro(n, sub_expr.collect(), diag, symbol_counters)
99        }
100        BuiltinMacroFunction::ArrayRemove => {
101            array_remove_macro(n, sub_expr.collect(), diag, symbol_counters)
102        }
103        BuiltinMacroFunction::ArrayInsert => {
104            array_insert_macro(n, sub_expr.collect(), diag, symbol_counters)
105        }
106        BuiltinMacroFunction::ArrayIndexOf => {
107            array_index_of_macro(n, sub_expr.collect(), diag, symbol_counters)
108        }
109        BuiltinMacroFunction::CustomMouseCursor => {
110            let mut has_error = None;
111            let hotspot_type_error = "The last two arguments to custom cursor must be an integer";
112
113            // Take the next argument if it passes `valid`, otherwise record an error.
114            let mut next_arg =
115                |valid: fn(&Type) -> bool, type_error: &'static str| match sub_expr.next() {
116                    Some((e, _)) if valid(&e.ty()) => e,
117                    Some(_) => {
118                        has_error.get_or_insert((n.to_source_location(), type_error));
119                        Expression::Invalid
120                    }
121                    None => {
122                        has_error.get_or_insert((n.to_source_location(), "Not enough arguments"));
123                        Expression::Invalid
124                    }
125                };
126
127            let image = next_arg(
128                |t| matches!(t, Type::Image),
129                "The first argument to custom cursor must be image",
130            );
131            let hotspot_x = next_arg(|t| t.can_convert(&Type::Int32), hotspot_type_error);
132            let hotspot_y = next_arg(|t| t.can_convert(&Type::Int32), hotspot_type_error);
133
134            let expr = Expression::MouseCursor(MouseCursorInner::CustomMouseCursor {
135                image: Box::new(image),
136                hotspot_x: Box::new(hotspot_x),
137                hotspot_y: Box::new(hotspot_y),
138            });
139            if let Some((_, n)) = sub_expr.next() {
140                has_error.get_or_insert((
141                    n.to_source_location(),
142                    "Too many arguments for custom cursor",
143                ));
144            }
145            if let Some((n, msg)) = has_error {
146                diag.push_error(msg.into(), &n);
147            }
148
149            expr
150        }
151        BuiltinMacroFunction::Spring => spring_macro(n, sub_expr.collect(), diag),
152    }
153}
154
155fn spring_macro(
156    node: &dyn Spanned,
157    args: Vec<(Expression, Option<NodeOrToken>)>,
158    diag: &mut BuildDiagnostics,
159) -> Expression {
160    let literal = |e: &Expression| match e {
161        Expression::NumberLiteral(val, Unit::None) => Some(*val),
162        _ => None,
163    };
164    let bounce = match args.as_slice() {
165        [(Expression::UnaryOp { sub, op: '-' }, _)] => literal(sub).map(|v| -v),
166        [(Expression::UnaryOp { sub, op: '+' }, _)] => literal(sub),
167        [(expr, _)] => literal(expr),
168        _ => None,
169    };
170    let Some(mut bounce) = bounce else {
171        diag.push_error("The spring curve needs a single number literal argument".into(), node);
172        return Expression::EasingCurve(EasingCurve::Spring(0.));
173    };
174    if !(-1.0..=1.0).contains(&bounce) {
175        let loc = args[0].1.as_ref().map_or(node, |n| n as &dyn Spanned);
176        diag.push_error("The bounce argument to spring curve must be between -1 and 1".into(), loc);
177        bounce = 0.;
178    }
179    Expression::EasingCurve(EasingCurve::Spring(bounce as f32))
180}
181
182fn min_max_macro(
183    node: &dyn Spanned,
184    op: MinMaxOp,
185    args: Vec<(Expression, Option<NodeOrToken>)>,
186    diag: &mut BuildDiagnostics,
187    symbol_counters: &SymbolCounters,
188) -> Expression {
189    if args.is_empty() {
190        diag.push_error("Needs at least one argument".into(), node);
191        return Expression::Invalid;
192    }
193    let ty = Expression::common_target_type_for_type_list(args.iter().map(|expr| expr.0.ty()));
194    if ty.as_unit_product().is_none() {
195        diag.push_error("Invalid argument type".into(), node);
196        return Expression::Invalid;
197    }
198    let mut args = args.into_iter();
199    let (base, arg_node) = args.next().unwrap();
200    let mut base = base.maybe_convert_to(ty.clone(), &arg_node, diag, symbol_counters);
201    for (next, arg_node) in args {
202        let rhs = next.maybe_convert_to(ty.clone(), &arg_node, diag, symbol_counters);
203        base = min_max_expression(base, rhs, op);
204    }
205    base
206}
207
208fn clamp_macro(
209    node: &dyn Spanned,
210    args: Vec<(Expression, Option<NodeOrToken>)>,
211    diag: &mut BuildDiagnostics,
212    symbol_counters: &SymbolCounters,
213) -> Expression {
214    if args.len() != 3 {
215        diag.push_error(
216            "`clamp` needs three values: the `value` to clamp, the `minimum` and the `maximum`"
217                .into(),
218            node,
219        );
220        return Expression::Invalid;
221    }
222    let (value, value_node) = args.first().unwrap().clone();
223    let ty = value.ty();
224    if ty.as_unit_product().is_none() {
225        diag.push_error("Invalid argument type".into(), &value_node);
226        return Expression::Invalid;
227    }
228
229    let (min, min_node) = args.get(1).unwrap().clone();
230    let min = min.maybe_convert_to(ty.clone(), &min_node, diag, symbol_counters);
231    let (max, max_node) = args.get(2).unwrap().clone();
232    let max = max.maybe_convert_to(ty.clone(), &max_node, diag, symbol_counters);
233
234    let value = min_max_expression(value, max, MinMaxOp::Min);
235    min_max_expression(min, value, MinMaxOp::Max)
236}
237
238fn mod_macro(
239    node: &dyn Spanned,
240    args: Vec<(Expression, Option<NodeOrToken>)>,
241    diag: &mut BuildDiagnostics,
242    symbol_counters: &SymbolCounters,
243) -> Expression {
244    if args.len() != 2 {
245        diag.push_error("Needs 2 arguments".into(), node);
246        return Expression::Invalid;
247    }
248    let (lhs_ty, rhs_ty) = (args[0].0.ty(), args[1].0.ty());
249    let common_ty = if lhs_ty.default_unit().is_some() {
250        lhs_ty
251    } else if rhs_ty.default_unit().is_some() {
252        rhs_ty
253    } else if matches!(lhs_ty, Type::UnitProduct(_)) {
254        lhs_ty
255    } else if matches!(rhs_ty, Type::UnitProduct(_)) {
256        rhs_ty
257    } else {
258        Type::Float32
259    };
260
261    let source_location = Some(node.to_source_location());
262    let function = Callable::Builtin(BuiltinFunction::Mod);
263    let arguments = args
264        .into_iter()
265        .map(|(e, n)| e.maybe_convert_to(common_ty.clone(), &n, diag, symbol_counters));
266    if matches!(common_ty, Type::Float32) {
267        Expression::FunctionCall { function, arguments: arguments.collect(), source_location }
268    } else {
269        Expression::Cast {
270            from: Expression::FunctionCall {
271                function,
272                arguments: arguments
273                    .map(|a| Expression::Cast { from: a.into(), to: Type::Float32 })
274                    .collect(),
275                source_location,
276            }
277            .into(),
278            to: common_ty.clone(),
279        }
280    }
281}
282
283fn abs_macro(
284    node: &dyn Spanned,
285    args: Vec<(Expression, Option<NodeOrToken>)>,
286    diag: &mut BuildDiagnostics,
287    symbol_counters: &SymbolCounters,
288) -> Expression {
289    if args.len() != 1 {
290        diag.push_error("Needs 1 argument".into(), node);
291        return Expression::Invalid;
292    }
293    let ty = args[0].0.ty();
294    let ty = if ty.default_unit().is_some() || matches!(ty, Type::UnitProduct(_)) {
295        ty
296    } else {
297        Type::Float32
298    };
299
300    let source_location = Some(node.to_source_location());
301    let function = Callable::Builtin(BuiltinFunction::Abs);
302    if matches!(ty, Type::Float32) {
303        let arguments = args
304            .into_iter()
305            .map(|(e, n)| e.maybe_convert_to(ty.clone(), &n, diag, symbol_counters))
306            .collect();
307        Expression::FunctionCall { function, arguments, source_location }
308    } else {
309        Expression::Cast {
310            from: Expression::FunctionCall {
311                function,
312                arguments: args
313                    .into_iter()
314                    .map(|(a, _)| Expression::Cast { from: a.into(), to: Type::Float32 })
315                    .collect(),
316                source_location,
317            }
318            .into(),
319            to: ty,
320        }
321    }
322}
323
324fn rgb_macro(
325    node: &dyn Spanned,
326    args: Vec<(Expression, Option<NodeOrToken>)>,
327    diag: &mut BuildDiagnostics,
328    symbol_counters: &SymbolCounters,
329) -> Expression {
330    if args.len() < 3 || args.len() > 4 {
331        diag.push_error(
332            format!("This function needs 3 or 4 arguments, but {} were provided", args.len()),
333            node,
334        );
335        return Expression::Invalid;
336    }
337    let mut arguments: Vec<_> = args
338        .into_iter()
339        .enumerate()
340        .map(|(i, (expr, n))| {
341            if i < 3 {
342                if expr.ty() == Type::Percent {
343                    Expression::BinaryExpression {
344                        lhs: Box::new(expr.maybe_convert_to(
345                            Type::Float32,
346                            &n,
347                            diag,
348                            symbol_counters,
349                        )),
350                        rhs: Box::new(Expression::NumberLiteral(255., Unit::None)),
351                        op: '*',
352                        source_location: None,
353                    }
354                } else {
355                    expr.maybe_convert_to(Type::Float32, &n, diag, symbol_counters)
356                }
357            } else {
358                expr.maybe_convert_to(Type::Float32, &n, diag, symbol_counters)
359            }
360        })
361        .collect();
362    if arguments.len() < 4 {
363        arguments.push(Expression::NumberLiteral(1., Unit::None))
364    }
365    Expression::FunctionCall {
366        function: BuiltinFunction::Rgb.into(),
367        arguments,
368        source_location: Some(node.to_source_location()),
369    }
370}
371
372fn hsv_macro(
373    node: &dyn Spanned,
374    args: Vec<(Expression, Option<NodeOrToken>)>,
375    diag: &mut BuildDiagnostics,
376    symbol_counters: &SymbolCounters,
377) -> Expression {
378    if args.len() < 3 || args.len() > 4 {
379        diag.push_error(
380            format!("This function needs 3 or 4 arguments, but {} were provided", args.len()),
381            node,
382        );
383        return Expression::Invalid;
384    }
385    let mut arguments: Vec<_> = args
386        .into_iter()
387        .enumerate()
388        .map(|(i, (expr, n))| {
389            // For hue (index 0), convert angle to degrees
390            if i == 0 && expr.ty() == Type::Angle {
391                Expression::BinaryExpression {
392                    lhs: Box::new(expr),
393                    rhs: Box::new(Expression::NumberLiteral(1., Unit::Deg)),
394                    op: '/',
395                    source_location: None,
396                }
397            } else {
398                expr.maybe_convert_to(Type::Float32, &n, diag, symbol_counters)
399            }
400        })
401        .collect();
402    if arguments.len() < 4 {
403        arguments.push(Expression::NumberLiteral(1., Unit::None))
404    }
405    Expression::FunctionCall {
406        function: BuiltinFunction::Hsv.into(),
407        arguments,
408        source_location: Some(node.to_source_location()),
409    }
410}
411
412fn oklch_macro(
413    node: &dyn Spanned,
414    args: Vec<(Expression, Option<NodeOrToken>)>,
415    diag: &mut BuildDiagnostics,
416    symbol_counters: &SymbolCounters,
417) -> Expression {
418    if args.len() < 3 || args.len() > 4 {
419        diag.push_error(
420            format!("This function needs 3 or 4 arguments, but {} were provided", args.len()),
421            node,
422        );
423        return Expression::Invalid;
424    }
425    let mut arguments: Vec<_> = args
426        .into_iter()
427        .enumerate()
428        .map(|(i, (expr, n))| {
429            // For chroma (index 1), 100% should equal 0.4, not 1.0
430            if i == 1 && expr.ty() == Type::Percent {
431                Expression::BinaryExpression {
432                    lhs: Box::new(expr),
433                    rhs: Box::new(Expression::NumberLiteral(0.004, Unit::None)),
434                    op: '*',
435                    source_location: None,
436                }
437            // For hue (index 2), convert angle to degrees
438            } else if i == 2 && expr.ty() == Type::Angle {
439                Expression::BinaryExpression {
440                    lhs: Box::new(expr),
441                    rhs: Box::new(Expression::NumberLiteral(1., Unit::Deg)),
442                    op: '/',
443                    source_location: None,
444                }
445            } else {
446                expr.maybe_convert_to(Type::Float32, &n, diag, symbol_counters)
447            }
448        })
449        .collect();
450    if arguments.len() < 4 {
451        arguments.push(Expression::NumberLiteral(1., Unit::None))
452    }
453    Expression::FunctionCall {
454        function: BuiltinFunction::Oklch.into(),
455        arguments,
456        source_location: Some(node.to_source_location()),
457    }
458}
459
460fn debug_macro(
461    node: &dyn Spanned,
462    args: Vec<(Expression, Option<NodeOrToken>)>,
463    diag: &mut BuildDiagnostics,
464    symbol_counters: &SymbolCounters,
465) -> Expression {
466    let mut string = None;
467    for (expr, node) in args {
468        let val = to_debug_string(expr, &node, diag, symbol_counters);
469        string = Some(match string {
470            None => val,
471            Some(string) => Expression::BinaryExpression {
472                lhs: Box::new(string),
473                op: '+',
474                rhs: Box::new(Expression::BinaryExpression {
475                    source_location: None,
476                    lhs: Box::new(Expression::StringLiteral(" ".into())),
477                    op: '+',
478                    rhs: Box::new(val),
479                }),
480                source_location: None,
481            },
482        });
483    }
484    Expression::FunctionCall {
485        function: BuiltinFunction::Debug.into(),
486        arguments: vec![
487            string.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)),
488        ],
489        source_location: Some(node.to_source_location()),
490    }
491}
492
493fn array_push_macro(
494    node: &dyn Spanned,
495    mut args: Vec<(Expression, Option<NodeOrToken>)>,
496    diag: &mut BuildDiagnostics,
497    symbol_counters: &SymbolCounters,
498) -> Expression {
499    if args.len() != 2 {
500        diag.push_error(
501            format!("This method needs 1 argument, but {} were provided", args.len() - 1),
502            node,
503        );
504        return Expression::Invalid;
505    }
506
507    let element_type =
508        if let Type::Array(t) = args[0].0.ty() { (*t).clone() } else { Type::Invalid };
509
510    let (model_expr, _) = args.remove(0);
511    let (value_expr, value_node) = args.remove(0);
512    let value = value_expr.maybe_convert_to(element_type, &value_node, diag, symbol_counters);
513    Expression::FunctionCall {
514        function: Callable::Builtin(BuiltinFunction::ArrayPush),
515        arguments: vec![model_expr, value],
516        source_location: Some(node.to_source_location()),
517    }
518}
519
520fn array_remove_macro(
521    node: &dyn Spanned,
522    mut args: Vec<(Expression, Option<NodeOrToken>)>,
523    diag: &mut BuildDiagnostics,
524    symbol_counters: &SymbolCounters,
525) -> Expression {
526    if args.len() != 2 {
527        diag.push_error(
528            format!("This method needs 1 argument, but {} were provided", args.len() - 1),
529            node,
530        );
531        return Expression::Invalid;
532    }
533
534    let (model_expr, _) = args.remove(0);
535    let (index_expr, index_node) = args.remove(0);
536    let index = index_expr.maybe_convert_to(Type::Int32, &index_node, diag, symbol_counters);
537    Expression::FunctionCall {
538        function: Callable::Builtin(BuiltinFunction::ArrayRemove),
539        arguments: vec![model_expr, index],
540        source_location: Some(node.to_source_location()),
541    }
542}
543
544fn array_insert_macro(
545    node: &dyn Spanned,
546    mut args: Vec<(Expression, Option<NodeOrToken>)>,
547    diag: &mut BuildDiagnostics,
548    symbol_counters: &SymbolCounters,
549) -> Expression {
550    if args.len() != 3 {
551        diag.push_error(
552            format!("This method needs 2 arguments, but {} were provided", args.len() - 1),
553            node,
554        );
555        return Expression::Invalid;
556    }
557
558    let element_type =
559        if let Type::Array(t) = args[0].0.ty() { (*t).clone() } else { Type::Invalid };
560
561    let (model_expr, _) = args.remove(0);
562    let (index_expr, index_node) = args.remove(0);
563    let (value_expr, value_node) = args.remove(0);
564    let index = index_expr.maybe_convert_to(Type::Int32, &index_node, diag, symbol_counters);
565    let value = value_expr.maybe_convert_to(element_type, &value_node, diag, symbol_counters);
566    Expression::FunctionCall {
567        function: Callable::Builtin(BuiltinFunction::ArrayInsert),
568        arguments: vec![model_expr, index, value],
569        source_location: Some(node.to_source_location()),
570    }
571}
572
573/// Unlike the other array macros, this lowers to `BuiltinFunction::ArrayFindIndex`
574/// (`array.find-index((x) => x == value)`), not a same-named builtin.
575fn array_index_of_macro(
576    node: &dyn Spanned,
577    mut args: Vec<(Expression, Option<NodeOrToken>)>,
578    diag: &mut BuildDiagnostics,
579    symbol_counters: &SymbolCounters,
580) -> Expression {
581    if args.len() != 2 {
582        diag.push_error(
583            format!("This method needs 1 argument, but {} were provided", args.len() - 1),
584            node,
585        );
586        return Expression::Invalid;
587    }
588
589    let element_type =
590        if let Type::Array(t) = args[0].0.ty() { (*t).clone() } else { Type::Invalid };
591
592    let (model_expr, _) = args.remove(0);
593    let (value_expr, value_node) = args.remove(0);
594    let value =
595        value_expr.maybe_convert_to(element_type.clone(), &value_node, diag, symbol_counters);
596
597    // Evaluate `value` once, before the search: the closure body runs once per row, so
598    // embedding `value` there directly would re-evaluate it per row instead of once.
599    let value_local = symbol_counters.generate_name("index_of_value_");
600    let arg_name = symbol_counters.generate_name("index_of_element_");
601    let predicate = Expression::Closure {
602        arg_name: arg_name.clone(),
603        expression: Box::new(Expression::BinaryExpression {
604            lhs: Box::new(Expression::ReadLocalVariable {
605                name: arg_name,
606                ty: element_type.clone(),
607            }),
608            rhs: Box::new(Expression::ReadLocalVariable {
609                name: value_local.clone(),
610                ty: element_type,
611            }),
612            op: '=',
613            source_location: None,
614        }),
615    };
616
617    Expression::CodeBlock(vec![
618        Expression::StoreLocalVariable { name: value_local, value: Box::new(value) },
619        Expression::FunctionCall {
620            function: Callable::Builtin(BuiltinFunction::ArrayFindIndex),
621            arguments: vec![model_expr, predicate],
622            source_location: Some(node.to_source_location()),
623        },
624    ])
625}
626
627fn to_debug_string(
628    expr: Expression,
629    node: &dyn Spanned,
630    diag: &mut BuildDiagnostics,
631    symbol_counters: &SymbolCounters,
632) -> Expression {
633    let ty = expr.ty();
634    match &ty {
635        Type::Invalid => Expression::Invalid,
636        Type::Void
637        | Type::InferredCallback
638        | Type::InferredProperty
639        | Type::Callback { .. }
640        | Type::ComponentFactory
641        | Type::Function { .. }
642        | Type::ElementReference
643        | Type::LayoutCache
644        | Type::ArrayOfU16
645        | Type::Model
646        | Type::PathData
647        | Type::Closure => {
648            diag.push_error("Cannot debug this expression".into(), node);
649            Expression::Invalid
650        }
651        Type::Float32 | Type::Int32 => {
652            expr.maybe_convert_to(Type::String, node, diag, symbol_counters)
653        }
654        Type::String => expr,
655        // TODO
656        Type::Color
657        | Type::Brush
658        | Type::Image
659        | Type::Easing
660        | Type::MouseCursor
661        | Type::StyledText
662        | Type::Array(_)
663        | Type::DataTransfer => {
664            Expression::StringLiteral("<debug-of-this-type-not-yet-implemented>".into())
665        }
666        Type::Duration
667        | Type::PhysicalLength
668        | Type::LogicalLength
669        | Type::Rem
670        | Type::Angle
671        | Type::Percent
672        | Type::UnitProduct(_) => Expression::BinaryExpression {
673            lhs: Box::new(
674                Expression::Cast { from: Box::new(expr), to: Type::Float32 }.maybe_convert_to(
675                    Type::String,
676                    node,
677                    diag,
678                    symbol_counters,
679                ),
680            ),
681            op: '+',
682            rhs: Box::new(Expression::StringLiteral(
683                Type::UnitProduct(ty.as_unit_product().unwrap()).to_smolstr(),
684            )),
685            source_location: None,
686        },
687        Type::Bool => Expression::Condition {
688            condition: Box::new(expr),
689            true_expr: Box::new(Expression::StringLiteral("true".into())),
690            false_expr: Box::new(Expression::StringLiteral("false".into())),
691            source_location: None,
692        },
693        Type::Struct(s) => {
694            let local_object = symbol_counters.generate_name("debug_struct");
695            let mut string = None;
696            for k in s.fields.keys() {
697                let field_name = if string.is_some() {
698                    format_smolstr!(", {}: ", k)
699                } else {
700                    format_smolstr!("{{ {}: ", k)
701                };
702                let value = to_debug_string(
703                    Expression::StructFieldAccess {
704                        base: Box::new(Expression::ReadLocalVariable {
705                            name: local_object.clone(),
706                            ty: ty.clone(),
707                        }),
708                        name: k.clone(),
709                    },
710                    node,
711                    diag,
712                    symbol_counters,
713                );
714                let field = Expression::BinaryExpression {
715                    lhs: Box::new(Expression::StringLiteral(field_name)),
716                    op: '+',
717                    rhs: Box::new(value),
718                    source_location: None,
719                };
720                string = Some(match string {
721                    None => field,
722                    Some(x) => Expression::BinaryExpression {
723                        lhs: Box::new(x),
724                        op: '+',
725                        rhs: Box::new(field),
726                        source_location: None,
727                    },
728                });
729            }
730            match string {
731                None => Expression::StringLiteral("{}".into()),
732                Some(string) => Expression::CodeBlock(vec![
733                    Expression::StoreLocalVariable { name: local_object, value: Box::new(expr) },
734                    Expression::BinaryExpression {
735                        source_location: None,
736                        lhs: Box::new(string),
737                        op: '+',
738                        rhs: Box::new(Expression::StringLiteral(" }".into())),
739                    },
740                ]),
741            }
742        }
743        Type::Enumeration(_) | Type::Keys => {
744            Expression::Cast { from: Box::new(expr), to: (Type::String) }
745        }
746    }
747}
748
749/// Generate an expression which is like `min(lhs, rhs)` if op is '<' or `max(lhs, rhs)` if op is '>'.
750/// counter is an unique id.
751/// The rhs and lhs of the expression must have the same numerical type
752pub fn min_max_expression(lhs: Expression, rhs: Expression, op: MinMaxOp) -> Expression {
753    let lhs_ty = lhs.ty();
754    let rhs_ty = rhs.ty();
755    let ty = match (lhs_ty, rhs_ty) {
756        (a, b) if a == b => a,
757        (Type::Int32, Type::Float32) | (Type::Float32, Type::Int32) => Type::Float32,
758        _ => Type::Invalid,
759    };
760    Expression::MinMax { ty, op, lhs: Box::new(lhs), rhs: Box::new(rhs) }
761}