Skip to main content

i_slint_compiler/passes/
resolving.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// cSpell: ignore depr descr idents shiftbehavior unaryop Unshiftable uppercased
5//! This pass resolves the property binding expressions.
6//!
7//! Before this pass, all the expression are of type Expression::Uncompiled,
8//! and there should no longer be Uncompiled expression after this pass.
9//!
10//! Most of the code for the resolving actually lies in the expression_tree module
11
12use crate::diagnostics::{BuildDiagnostics, Spanned};
13use crate::expression_tree::*;
14use crate::langtype;
15use crate::langtype::{
16    ElementType, KeyboardModifiers, PropertyLookupMode, Struct, StructName, Type,
17};
18use crate::lookup::{LookupCtx, LookupObject, LookupResult, LookupResultCallable};
19use crate::object_tree::*;
20use crate::parser::{
21    NodeOrToken, SyntaxKind, SyntaxNode, TextRange, identifier_text, syntax_nodes,
22};
23use crate::symbol_counters::SymbolCounters;
24use crate::typeregister::TypeRegister;
25use core::num::IntErrorKind;
26use smol_str::{SmolStr, ToSmolStr};
27use std::collections::BTreeMap;
28use std::rc::Rc;
29use std::sync::Arc;
30use unicode_segmentation::UnicodeSegmentation;
31
32mod remove_noop;
33
34/// This represents a scope for the Component, where Component is the repeated component, but
35/// does not represent a component in the .slint file
36#[derive(Clone)]
37struct ComponentScope(Vec<ElementRc>);
38
39fn resolve_expression(
40    elem: &ElementRc,
41    expr: &mut Expression,
42    property_name: Option<&str>,
43    property_type: Type,
44    scope: &[ElementRc],
45    type_register: &TypeRegister,
46    type_loader: &crate::typeloader::TypeLoader,
47    diag: &mut BuildDiagnostics,
48) {
49    if let Expression::Uncompiled(node) = expr.ignore_debug_hooks() {
50        let mut lookup_ctx = LookupCtx {
51            property_name,
52            property_type,
53            expected_type: Type::default(),
54            component_scope: scope,
55            diag,
56            symbol_counters: type_loader.symbol_counters.clone(),
57            arguments: Vec::new(),
58            type_register,
59            type_loader: Some(type_loader),
60            current_token: None,
61            local_variables: Vec::new(),
62            expected_type_probe: None,
63        };
64        lookup_ctx.expected_type = lookup_ctx.return_type().clone();
65
66        let new_expr = match node.kind() {
67            SyntaxKind::CallbackConnection => {
68                let node = syntax_nodes::CallbackConnection::from(node.clone());
69                if let Some(property_name) = property_name {
70                    check_callback_alias_validity(&node, elem, property_name, lookup_ctx.diag);
71                }
72                let expr = Expression::from_callback_connection(node.clone(), &mut lookup_ctx);
73                #[cfg(feature = "slint-sc")]
74                check_slint_sc_handler_body(&expr, &node, &mut lookup_ctx);
75                expr
76            }
77            SyntaxKind::Function => Expression::from_function(node.clone().into(), &mut lookup_ctx),
78            SyntaxKind::Expression => {
79                //FIXME again: this happen for non-binding expression (i.e: model)
80                Expression::from_expression_node(node.clone().into(), &mut lookup_ctx)
81                    .maybe_convert_to(
82                        lookup_ctx.property_type.clone(),
83                        node,
84                        lookup_ctx.diag,
85                        &lookup_ctx.symbol_counters,
86                    )
87            }
88            SyntaxKind::BindingExpression => {
89                Expression::from_binding_expression_node(node.clone(), &mut lookup_ctx)
90            }
91            SyntaxKind::PropertyChangedCallback => {
92                let node = syntax_nodes::PropertyChangedCallback::from(node.clone());
93                if let Some(code_block_node) = node.CodeBlock() {
94                    Expression::from_codeblock_node(code_block_node, &mut lookup_ctx)
95                } else if let Some(expr_node) = node.Expression() {
96                    Expression::from_expression_node(expr_node, &mut lookup_ctx)
97                } else {
98                    assert!(diag.has_errors());
99                    Expression::Invalid
100                }
101            }
102            SyntaxKind::TwoWayBinding => {
103                assert!(
104                    diag.has_errors(),
105                    "Two way binding should have been resolved already  (property: {property_name:?})"
106                );
107                Expression::Invalid
108            }
109            SyntaxKind::AtKeys => {
110                Expression::from_at_keys_node(node.clone().into(), &mut lookup_ctx)
111            }
112            _ => {
113                debug_assert!(diag.has_errors());
114                Expression::Invalid
115            }
116        };
117        match expr {
118            Expression::DebugHook { expression, .. } => **expression = new_expr,
119            _ => *expr = new_expr,
120        }
121    }
122}
123
124/// Resolve the subject and the case values to create a standard conditional element
125fn resolve_match_elements(
126    elem: &ElementRc,
127    scope: &[ElementRc],
128    type_register: &TypeRegister,
129    type_loader: &crate::typeloader::TypeLoader,
130    diag: &mut BuildDiagnostics,
131) {
132    let mut match_elements = std::mem::take(&mut elem.borrow_mut().match_elements);
133    for match_element in &mut match_elements {
134        if match_element.cases.is_empty()
135            && matches!(match_element.wildcard, WildcardMatchCaseInfo::None)
136        {
137            continue;
138        }
139        resolve_expression(
140            elem,
141            &mut match_element.subject,
142            None,
143            Type::Invalid,
144            scope,
145            type_register,
146            type_loader,
147            diag,
148        );
149        let case_type = match_element.subject.ty();
150        for case in &mut match_element.cases {
151            resolve_expression(
152                elem,
153                &mut case.value,
154                None,
155                case_type.clone(),
156                scope,
157                type_register,
158                type_loader,
159                diag,
160            );
161            check_case_value(&case.value, &case.node, diag);
162        }
163        let values: Vec<Option<CaseValue>> =
164            match_element.cases.iter().map(|case| CaseValue::new(&case.value)).collect();
165        check_duplicate_cases(&match_element.cases, &values, diag);
166        check_exhaustiveness(match_element, &values, diag);
167
168        let subject_ref = crate::layout::create_new_prop(elem, "match-subject".into(), case_type);
169        let subject = std::mem::replace(
170            &mut match_element.subject,
171            Expression::PropertyReference(subject_ref.clone()),
172        );
173        elem.borrow_mut().set_binding(subject_ref.name().clone(), subject.into());
174
175        match_element.lower_to_conditional_elements();
176    }
177}
178
179/// Confirms that each case is a literal value and matches the type of the subject
180fn check_case_value(value: &Expression, node: &SyntaxNode, diag: &mut BuildDiagnostics) {
181    let is_literal = as_number_literal(value).is_some()
182        || matches!(
183            value,
184            Expression::StringLiteral(..)
185                | Expression::BoolLiteral(..)
186                | Expression::EnumerationValue(..)
187        );
188    let is_valid_cast = matches!(
189        value,
190        Expression::Cast { from, to, .. }
191            if as_number_literal(from).is_some()
192                && matches!(to, Type::Color | Type::Int32)
193    );
194
195    if let Some((number, Unit::None)) = as_number_literal(value)
196        && number.fract() != 0.0
197    {
198        diag.push_warning("Floating point comparison is not recommended".into(), node);
199    }
200
201    if is_literal || is_valid_cast {
202        // pass
203    } else if matches!(value, Expression::Cast { .. }) {
204        diag.push_error("Cannot perform type conversion".into(), node);
205    } else {
206        diag.push_error("Cases must be literal values".into(), node);
207    }
208}
209
210fn as_number_literal(value: &Expression) -> Option<(f64, Unit)> {
211    match value {
212        Expression::NumberLiteral(number, unit) => Some((*number, *unit)),
213        Expression::UnaryOp { sub, op: '-' } => as_number_literal(sub).map(|(n, u)| (-n, u)),
214        _ => None,
215    }
216}
217
218#[derive(PartialEq)]
219enum CaseValue {
220    Number(f64, Unit),
221    String(SmolStr),
222    Bool(bool),
223    Enumeration(langtype::EnumerationValue),
224}
225
226impl CaseValue {
227    fn new(value: &Expression) -> Option<Self> {
228        match value {
229            Expression::Cast { from, .. } => Self::new(from),
230            Expression::UnaryOp { sub, op: '-' } => match Self::new(sub)? {
231                Self::Number(number, unit) => Some(Self::Number(-number, unit)),
232                _ => None,
233            },
234            Expression::NumberLiteral(number, unit) => Some(Self::Number(*number, *unit)),
235            Expression::StringLiteral(string) => Some(Self::String(string.clone())),
236            Expression::BoolLiteral(boolean) => Some(Self::Bool(*boolean)),
237            Expression::EnumerationValue(value) => Some(Self::Enumeration(value.clone())),
238            _ => None, // For invalid non-literals
239        }
240    }
241}
242
243/// Reports every case whose value is already covered by an earlier case
244fn check_duplicate_cases(
245    cases: &[MatchCaseInfo],
246    values: &[Option<CaseValue>],
247    diag: &mut BuildDiagnostics,
248) {
249    let mut seen: Vec<&CaseValue> = Vec::with_capacity(values.len());
250    for (case, value) in cases.iter().zip(values) {
251        let Some(value) = value else {
252            continue; // not a valid literal
253        };
254        if seen.contains(&value) {
255            diag.push_error("Duplicate case value".into(), &case.node);
256        } else {
257            seen.push(value);
258        }
259    }
260}
261
262impl std::fmt::Display for CaseValue {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        match self {
265            CaseValue::Number(number, _) => write!(f, "{number}"),
266            CaseValue::String(string) => write!(f, "{string:?}"),
267            CaseValue::Bool(boolean) => write!(f, "{boolean}"),
268            CaseValue::Enumeration(value) => write!(f, "{value}"),
269        }
270    }
271}
272
273/// Reports a match element that does not cover every value its subject can take
274fn check_exhaustiveness(
275    match_element: &MatchElementInfo,
276    values: &[Option<CaseValue>],
277    diag: &mut BuildDiagnostics,
278) {
279    if !matches!(match_element.wildcard, WildcardMatchCaseInfo::None) {
280        return;
281    }
282    // Prevents duplicated errors if both not a literal and not exhaustive
283    let mut covered: Vec<&CaseValue> = Vec::with_capacity(values.len());
284    for value in values {
285        let Some(value) = value else {
286            return;
287        };
288        covered.push(value);
289    }
290    let subject_node = match_element.node.Expression();
291    let subject_type = match_element.subject.ty();
292    let expected: Vec<CaseValue> = match &subject_type {
293        Type::Bool => vec![CaseValue::Bool(true), CaseValue::Bool(false)],
294        Type::Enumeration(enumeration) => (0..enumeration.values.len())
295            .map(|value| {
296                CaseValue::Enumeration(langtype::EnumerationValue {
297                    value,
298                    enumeration: enumeration.clone(),
299                })
300            })
301            .collect(),
302        // The subject expression failed to resolve, so an error was already reported
303        Type::Invalid => return,
304        _ => {
305            diag.push_error(
306                format!("Non-exhaustive match on {subject_type}: a '*' case is required"),
307                &subject_node,
308            );
309            return;
310        }
311    };
312
313    let mut missing = Vec::new();
314    for value in &expected {
315        if !covered.contains(&value) {
316            missing.push(format!("'{value}'"));
317        }
318    }
319    if !missing.is_empty() {
320        diag.push_error(
321            format!("Non-exhaustive match on {subject_type}: missing {}", missing.join(", ")),
322            &subject_node,
323        );
324    }
325}
326
327/// Call the visitor for each children of the element recursively, starting with the element itself
328///
329/// The item that is being visited will be pushed to the scope and popped once visitation is over.
330fn recurse_elem_with_scope(
331    elem: &ElementRc,
332    mut scope: ComponentScope,
333    vis: &mut impl FnMut(&ElementRc, &ComponentScope),
334) -> ComponentScope {
335    scope.0.push(elem.clone());
336    vis(elem, &scope);
337    for sub in &elem.borrow().children {
338        scope = recurse_elem_with_scope(sub, scope, vis);
339    }
340    scope.0.pop();
341    scope
342}
343
344pub fn resolve_expressions(
345    doc: &Document,
346    type_loader: &crate::typeloader::TypeLoader,
347    diag: &mut BuildDiagnostics,
348) {
349    for component in doc.inner_components.iter() {
350        recurse_elem_with_scope(
351            &component.root_element,
352            ComponentScope(Vec::new()),
353            &mut |elem, scope| {
354                // Resolve the model expression (of a `for`) with the parent
355                // scope, and before the two-way bindings below so they can
356                // type-check field accesses against the model row type.
357                if elem.borrow().repeated.is_some() {
358                    debug_assert!(scope.0.len() > 1);
359                    let parent_scope = &scope.0[..scope.0.len() - 1];
360                    visit_repeater_model_expression(elem, |expr, property_name, property_type| {
361                        resolve_expression(
362                            elem,
363                            expr,
364                            property_name,
365                            property_type(),
366                            parent_scope,
367                            &doc.local_registry,
368                            type_loader,
369                            diag,
370                        );
371                    });
372                }
373
374                resolve_match_elements(elem, &scope.0, &doc.local_registry, type_loader, diag);
375
376                resolve_two_way_bindings_for_element(elem, &scope.0, &doc.local_registry, diag);
377
378                visit_element_expressions_excluding_repeater_model(
379                    elem,
380                    |expr, property_name, property_type| {
381                        resolve_expression(
382                            elem,
383                            expr,
384                            property_name,
385                            property_type(),
386                            &scope.0,
387                            &doc.local_registry,
388                            type_loader,
389                            diag,
390                        );
391                    },
392                );
393            },
394        );
395    }
396}
397
398/// To be used in [`Expression::from_qualified_name_node`] to specify if the lookup is performed
399/// for two ways binding (which happens before the models and other expressions are resolved),
400/// or after that.
401#[derive(Default)]
402enum LookupPhase {
403    #[default]
404    UnspecifiedPhase,
405    ResolvingTwoWayBindings,
406}
407
408/// The range of `node`, extended to cover any blank space before it so a cursor there
409/// (like the empty rhs of `x ==  `) still resolves to this node.
410fn probe_range(node: &SyntaxNode) -> TextRange {
411    let range = node.text_range();
412    let mut start = range.start();
413    let mut prev = node.node.prev_sibling_or_token();
414    while let Some(rowan::NodeOrToken::Token(t)) = &prev {
415        if !matches!(t.kind(), SyntaxKind::Whitespace | SyntaxKind::Comment) {
416            break;
417        }
418        start = t.text_range().start();
419        prev = t.prev_sibling_or_token();
420    }
421    TextRange::new(start, range.end())
422}
423
424impl Expression {
425    pub fn from_binding_expression_node(node: SyntaxNode, ctx: &mut LookupCtx) -> Self {
426        debug_assert_eq!(node.kind(), SyntaxKind::BindingExpression);
427        let e = node
428            .children()
429            .find_map(|n| match n.kind() {
430                SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
431                SyntaxKind::CodeBlock => Some(Self::from_codeblock_node(n.into(), ctx)),
432                _ => None,
433            })
434            .unwrap_or(Self::Invalid);
435        if ctx.property_type == Type::LogicalLength && e.ty() == Type::Percent {
436            // See if a conversion from percentage to length is allowed
437            const RELATIVE_TO_PARENT_PROPERTIES: &[&str] =
438                &["width", "height", "preferred-width", "preferred-height"];
439            let property_name = ctx.property_name.unwrap_or_default();
440            if RELATIVE_TO_PARENT_PROPERTIES.contains(&property_name) {
441                return e;
442            } else {
443                ctx.diag.push_error(
444                    format!(
445                        "Automatic conversion from percentage to length is only possible for the following properties: {}",
446                        RELATIVE_TO_PARENT_PROPERTIES.join(", ")
447                    ),
448                    &node
449                );
450                return Expression::Invalid;
451            }
452        };
453        if !matches!(ctx.property_type, Type::Callback { .. } | Type::Function { .. }) {
454            e.maybe_convert_to(ctx.property_type.clone(), &node, ctx.diag, &ctx.symbol_counters)
455        } else {
456            // Binding to a callback or function shouldn't happen
457            assert!(ctx.diag.has_errors());
458            e
459        }
460    }
461
462    fn from_codeblock_node(node: syntax_nodes::CodeBlock, ctx: &mut LookupCtx) -> Expression {
463        debug_assert_eq!(node.kind(), SyntaxKind::CodeBlock);
464
465        // new scope for locals
466        ctx.local_variables.push(Vec::new());
467
468        let mut statements_or_exprs = node
469            .children()
470            .filter_map(|n| match n.kind() {
471                SyntaxKind::Expression => {
472                    Some((n.clone(), Self::from_expression_node(n.into(), ctx)))
473                }
474                SyntaxKind::ReturnStatement => {
475                    Some((n.clone(), Self::from_return_statement(n.into(), ctx)))
476                }
477                SyntaxKind::LetStatement => {
478                    Some((n.clone(), Self::from_let_statement(n.into(), ctx)))
479                }
480                _ => None,
481            })
482            .collect::<Vec<_>>();
483
484        remove_noop::remove_from_codeblock(&mut statements_or_exprs, ctx.diag);
485
486        let mut statements_or_exprs = statements_or_exprs
487            .into_iter()
488            .map(|(_node, statement_or_expr)| statement_or_expr)
489            .collect::<Vec<_>>();
490
491        let exit_points_and_return_types = statements_or_exprs
492            .iter()
493            .enumerate()
494            .filter_map(|(index, statement_or_expr)| {
495                if index == statements_or_exprs.len()
496                    || matches!(statement_or_expr, Expression::ReturnStatement(..))
497                {
498                    Some((index, statement_or_expr.ty()))
499                } else {
500                    None
501                }
502            })
503            .collect::<Vec<_>>();
504
505        let common_return_type = Self::common_target_type_for_type_list(
506            exit_points_and_return_types.iter().map(|(_, ty)| ty.clone()),
507        );
508
509        exit_points_and_return_types.into_iter().for_each(|(index, _)| {
510            let mut expr = std::mem::replace(&mut statements_or_exprs[index], Expression::Invalid);
511            expr = expr.maybe_convert_to(
512                common_return_type.clone(),
513                &node,
514                ctx.diag,
515                &ctx.symbol_counters,
516            );
517            statements_or_exprs[index] = expr;
518        });
519
520        // pop local scope
521        ctx.local_variables.pop();
522
523        Expression::CodeBlock(statements_or_exprs)
524    }
525
526    fn from_let_statement(node: syntax_nodes::LetStatement, ctx: &mut LookupCtx) -> Expression {
527        let name = identifier_text(&node.DeclaredIdentifier()).unwrap_or_default();
528
529        let global_lookup = crate::lookup::global_lookup();
530        if let Some(LookupResult::Expression {
531            expression:
532                Expression::ReadLocalVariable { .. } | Expression::FunctionParameterReference { .. },
533            ..
534        }) = global_lookup.lookup(ctx, &name)
535        {
536            ctx.diag
537                .push_error("Redeclaration of local variables is not allowed".to_string(), &node);
538            return Expression::Invalid;
539        }
540
541        // prefix with "local_" to avoid conflicts
542        let name: SmolStr = format!("local_{name}",).into();
543
544        let declared_ty = node.Type().map(|ty| type_from_node(ty, ctx.diag, ctx.type_register));
545        let value = match &declared_ty {
546            Some(t) => ctx.with_expected_type(t.clone(), |ctx| {
547                Self::from_expression_node(node.Expression(), ctx)
548            }),
549            None => Self::from_expression_node(node.Expression(), ctx),
550        };
551        let ty = declared_ty.unwrap_or_else(|| value.ty());
552
553        // we can get the last scope exists, because each codeblock creates a new scope and we are inside a codeblock here by necessity
554        ctx.local_variables.last_mut().unwrap().push((name.clone(), ty.clone()));
555
556        let value =
557            Box::new(value.maybe_convert_to(ty.clone(), &node, ctx.diag, &ctx.symbol_counters));
558
559        Expression::StoreLocalVariable { name, value }
560    }
561
562    fn from_return_statement(
563        node: syntax_nodes::ReturnStatement,
564        ctx: &mut LookupCtx,
565    ) -> Expression {
566        let return_type = ctx.return_type().clone();
567        let e = node.Expression();
568        if e.is_none() && !matches!(return_type, Type::Void | Type::Invalid) {
569            ctx.diag.push_error(format!("Must return a value of type '{return_type}'"), &node);
570        }
571        Expression::ReturnStatement(e.map(|n| {
572            let e = ctx
573                .with_expected_type(return_type.clone(), |ctx| Self::from_expression_node(n, ctx));
574            Box::new(e.maybe_convert_to(return_type, &node, ctx.diag, &ctx.symbol_counters))
575        }))
576    }
577
578    fn from_callback_connection(
579        node: syntax_nodes::CallbackConnection,
580        ctx: &mut LookupCtx,
581    ) -> Expression {
582        ctx.arguments =
583            node.DeclaredIdentifier().map(|x| identifier_text(&x).unwrap_or_default()).collect();
584        if let Some(code_block_node) = node.CodeBlock() {
585            Self::from_codeblock_node(code_block_node, ctx).maybe_convert_to(
586                ctx.return_type().clone(),
587                &node,
588                ctx.diag,
589                &ctx.symbol_counters,
590            )
591        } else if let Some(expr_node) = node.Expression() {
592            Self::from_expression_node(expr_node, ctx).maybe_convert_to(
593                ctx.return_type().clone(),
594                &node,
595                ctx.diag,
596                &ctx.symbol_counters,
597            )
598        } else {
599            Expression::Invalid
600        }
601    }
602
603    fn from_function(node: syntax_nodes::Function, ctx: &mut LookupCtx) -> Expression {
604        ctx.arguments = node
605            .ArgumentDeclaration()
606            .map(|x| identifier_text(&x.DeclaredIdentifier()).unwrap_or_default())
607            .collect();
608        let Some(code_block) = node.CodeBlock() else {
609            debug_assert!(ctx.diag.has_errors());
610            return Expression::Invalid;
611        };
612        Self::from_codeblock_node(code_block, ctx).maybe_convert_to(
613            ctx.return_type().clone(),
614            &node,
615            ctx.diag,
616            &ctx.symbol_counters,
617        )
618    }
619
620    pub fn from_expression_node(node: syntax_nodes::Expression, ctx: &mut LookupCtx) -> Self {
621        // LSP probe: the innermost node containing the offset wins (depth-first descent).
622        if ctx.expected_type_probe.is_some() {
623            let ty = ctx.expected_type.clone();
624            ctx.record_expected_type_probe(probe_range(&node), &ty);
625        }
626
627        // This function recurses for nested expressions. Dispatch with early returns
628        // instead of a `find_map` closure: in unoptimized builds, every arm of a match
629        // producing a value gets its own stack slot for the resulting `Expression`,
630        // adding up to a frame so large that deeply nested expressions overflow the
631        // stack. A `return` writes directly into the return slot instead.
632        for child in node.children_with_tokens() {
633            match child {
634                NodeOrToken::Node(node) => match node.kind() {
635                    SyntaxKind::Expression => return Self::from_expression_node(node.into(), ctx),
636                    SyntaxKind::AtImageUrl => {
637                        return Self::from_at_image_url_node(node.into(), ctx);
638                    }
639                    SyntaxKind::AtGradient => {
640                        #[cfg(feature = "slint-sc")]
641                        ctx.diag.slint_sc_error("@gradient expressions are", &node);
642                        return Self::from_at_gradient(node.into(), ctx);
643                    }
644                    SyntaxKind::AtTr => {
645                        #[cfg(feature = "slint-sc")]
646                        ctx.diag.slint_sc_error("@tr() expressions are", &node);
647                        return Self::from_at_tr(node.into(), ctx);
648                    }
649                    SyntaxKind::AtMarkdown => {
650                        #[cfg(feature = "slint-sc")]
651                        ctx.diag.slint_sc_error("@markdown() expressions are", &node);
652                        return Self::from_at_markdown(node.into(), ctx);
653                    }
654                    SyntaxKind::AtKeys => {
655                        #[cfg(feature = "slint-sc")]
656                        ctx.diag.slint_sc_error("@keys() expressions are", &node);
657                        return Self::from_at_keys_node(node.into(), ctx);
658                    }
659                    SyntaxKind::QualifiedName => {
660                        return Self::from_qualified_name_node(node.into(), ctx);
661                    }
662                    SyntaxKind::FunctionCallExpression => {
663                        let expr = Self::from_function_call_node(node.clone().into(), ctx);
664                        // Invoking a callback from a handler is the one call the
665                        // Slint SC subset has.
666                        #[cfg(feature = "slint-sc")]
667                        if !matches!(
668                            (&expr, &ctx.property_type),
669                            (Expression::Invalid, _)
670                                | (
671                                    Expression::FunctionCall {
672                                        function: Callable::Callback(..),
673                                        ..
674                                    },
675                                    Type::Callback(..)
676                                )
677                        ) {
678                            ctx.diag.slint_sc_error("Function calls are", &node);
679                        }
680                        return expr;
681                    }
682                    SyntaxKind::MemberAccess => {
683                        return Self::from_member_access_node(node.into(), ctx);
684                    }
685                    SyntaxKind::IndexExpression => {
686                        #[cfg(feature = "slint-sc")]
687                        ctx.diag.slint_sc_error("Index expressions are", &node);
688                        return Self::from_index_expression_node(node.into(), ctx);
689                    }
690                    SyntaxKind::SelfAssignment => {
691                        #[cfg(feature = "slint-sc")]
692                        ctx.diag.slint_sc_error("Self-assignment expressions are", &node);
693                        return Self::from_self_assignment_node(node.into(), ctx);
694                    }
695                    SyntaxKind::BinaryExpression => {
696                        return Self::from_binary_expression_node(node.into(), ctx);
697                    }
698                    SyntaxKind::UnaryOpExpression => {
699                        // Every unary operator (`+`, `-`, `!`) is in the Slint SC
700                        // subset, so there is nothing to reject here.
701                        return Self::from_unaryop_expression_node(node.into(), ctx);
702                    }
703                    SyntaxKind::ConditionalExpression => {
704                        // A conditional is in the Slint SC subset; its condition,
705                        // branches, and result type are each restricted on their own.
706                        return Self::from_conditional_expression_node(node.into(), ctx);
707                    }
708                    SyntaxKind::ObjectLiteral => {
709                        return Self::from_object_literal_node(node.into(), ctx);
710                    }
711                    SyntaxKind::Array => {
712                        #[cfg(feature = "slint-sc")]
713                        ctx.diag.slint_sc_error("Array expressions are", &node);
714                        return Self::from_array_node(node.into(), ctx);
715                    }
716                    SyntaxKind::CodeBlock => {
717                        #[cfg(feature = "slint-sc")]
718                        ctx.diag.slint_sc_error("Code blocks are", &node);
719                        return Self::from_codeblock_node(node.into(), ctx);
720                    }
721                    SyntaxKind::StringTemplate => {
722                        #[cfg(feature = "slint-sc")]
723                        ctx.diag.slint_sc_error("String interpolation expressions are", &node);
724                        return Self::from_string_template_node(node.into(), ctx);
725                    }
726                    SyntaxKind::Closure => {
727                        return Self::from_closure_node(node.into(), ctx, None);
728                    }
729                    _ => {}
730                },
731                NodeOrToken::Token(token) => match token.kind() {
732                    SyntaxKind::StringLiteral => {
733                        #[cfg(feature = "slint-sc")]
734                        ctx.diag.slint_sc_error("String literals are", &token);
735                        return crate::literals::unescape_string_reporting(
736                            Some(&token),
737                            ctx.diag,
738                            &token,
739                        )
740                        .map(Self::StringLiteral)
741                        .unwrap_or(Self::Invalid);
742                    }
743                    SyntaxKind::NumberLiteral => {
744                        return match crate::literals::parse_number_literal(token.text().into()) {
745                            Ok((value, unit)) => {
746                                #[cfg(feature = "slint-sc")]
747                                {
748                                    use crate::expression_tree::WrittenUnit;
749                                    match unit {
750                                        WrittenUnit::Px if value.fract() != 0. => ctx
751                                            .diag
752                                            .slint_sc_error("Non-integral lengths are", &token),
753                                        WrittenUnit::Px => {}
754                                        // A unit-less integer is an `int` literal; a
755                                        // fractional one would be a `float`.
756                                        WrittenUnit::None if value.fract() != 0. => ctx
757                                            .diag
758                                            .slint_sc_error("Non-integral numbers are", &token),
759                                        WrittenUnit::None => {}
760                                        _ => ctx.diag.slint_sc_error(
761                                            &format!("Number literals with the unit '{unit}' are"),
762                                            &token,
763                                        ),
764                                    }
765                                }
766                                let (value, unit) = unit.normalize(value);
767                                Expression::NumberLiteral(value, unit)
768                            }
769                            Err(e) => {
770                                ctx.diag.push_error(e.to_string(), &node);
771                                Self::Invalid
772                            }
773                        };
774                    }
775                    SyntaxKind::ColorLiteral => {
776                        return i_slint_common::color_parsing::parse_color_literal(token.text())
777                            .map(|i| Expression::Cast {
778                                from: Box::new(Expression::NumberLiteral(i as _, Unit::None)),
779                                to: Type::Color,
780                            })
781                            .unwrap_or_else(|| {
782                                ctx.diag.push_error("Invalid color literal".into(), &node);
783                                Self::Invalid
784                            });
785                    }
786
787                    _ => {}
788                },
789            }
790        }
791        Self::Invalid
792    }
793
794    fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self {
795        let Some(s) = crate::literals::unescape_string_reporting(
796            node.child_token(SyntaxKind::StringLiteral).as_ref(),
797            ctx.diag,
798            &node,
799        ) else {
800            return Self::Invalid;
801        };
802
803        if s.is_empty() {
804            return Expression::ImageReference {
805                resource_ref: ImageReference::None,
806                source_location: Some(node.to_source_location()),
807                nine_slice: None,
808            };
809        }
810
811        let resource_ref = if s.starts_with("data:") {
812            ImageReference::DataUri(s)
813        } else {
814            let absolute_source_path = {
815                let path = std::path::Path::new(&s);
816                if crate::pathutils::is_absolute(path) {
817                    s
818                } else {
819                    ctx.type_loader
820                        .and_then(|loader| {
821                            loader.resolve_import_path(Some(&(*node).clone().into()), &s)
822                        })
823                        .map(|i| i.0.to_string_lossy().into())
824                        .unwrap_or_else(|| {
825                            crate::pathutils::join(
826                                &crate::pathutils::dirname(node.source_file.path()),
827                                path,
828                            )
829                            .map(|p| p.to_string_lossy().into())
830                            .unwrap_or(s.clone())
831                        })
832                }
833            };
834            ImageReference::from_resolved(absolute_source_path)
835        };
836
837        // Slint SC decodes the image at compile time, so only a file on disk
838        // can be referenced.
839        #[cfg(feature = "slint-sc")]
840        match &resource_ref {
841            ImageReference::DataUri(_) => {
842                ctx.diag.slint_sc_error("Data URIs in @image-url() are", &node)
843            }
844            ImageReference::Url(_) => ctx.diag.slint_sc_error("URLs in @image-url() are", &node),
845            _ => {}
846        }
847
848        let nine_slice = node
849            .children_with_tokens()
850            .filter_map(|n| n.into_token())
851            .filter(|t| t.kind() == SyntaxKind::NumberLiteral)
852            .map(|arg| {
853                arg.text().parse().unwrap_or_else(|err: std::num::ParseIntError| {
854                    match err.kind() {
855                        IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => {
856                            ctx.diag.push_error("Number too big".into(), &arg)
857                        }
858                        IntErrorKind::InvalidDigit => ctx.diag.push_error(
859                            "Border widths of a nine-slice can't have units".into(),
860                            &arg,
861                        ),
862                        _ => ctx.diag.push_error("Cannot parse number literal".into(), &arg),
863                    };
864                    0u16
865                })
866            })
867            .collect::<Vec<u16>>();
868
869        let nine_slice = match nine_slice.as_slice() {
870            [x] => Some([*x, *x, *x, *x]),
871            [x, y] => Some([*x, *y, *x, *y]),
872            [x, y, z, w] => Some([*x, *y, *z, *w]),
873            [] => None,
874            _ => {
875                assert!(ctx.diag.has_errors());
876                None
877            }
878        };
879
880        #[cfg(feature = "slint-sc")]
881        if nine_slice.is_some() {
882            ctx.diag.slint_sc_error("Nine-slice borders in @image-url() are", &node);
883        }
884
885        Expression::ImageReference {
886            resource_ref,
887            source_location: Some(node.to_source_location()),
888            nine_slice,
889        }
890    }
891
892    pub fn from_at_gradient(node: syntax_nodes::AtGradient, ctx: &mut LookupCtx) -> Self {
893        enum GradKind {
894            Linear {
895                angle: Box<Expression>,
896            },
897            Radial {
898                center: Option<(Box<Expression>, Box<Expression>)>,
899                radius: Option<Box<Expression>>,
900            },
901            Conic {
902                from_angle: Box<Expression>,
903                center: Option<(Box<Expression>, Box<Expression>)>,
904            },
905        }
906
907        let all_subs: Vec<_> = node
908            .children_with_tokens()
909            .filter(|n| matches!(n.kind(), SyntaxKind::Comma | SyntaxKind::Expression))
910            .collect();
911
912        let grad_token = node.child_token(SyntaxKind::Identifier).unwrap();
913        let grad_text = grad_token.text();
914
915        // Helper: parse two consecutive length expressions at positions idx and idx+1
916        let parse_at_center = |idx: usize,
917                               ctx: &mut LookupCtx|
918         -> Option<(Box<Expression>, Box<Expression>)> {
919            let cx_node = all_subs.get(idx)?;
920            let cy_node = all_subs.get(idx + 1)?;
921            if cx_node.kind() != SyntaxKind::Expression || cy_node.kind() != SyntaxKind::Expression
922            {
923                return None;
924            }
925            let cx_syn = syntax_nodes::Expression::from(cx_node.as_node().unwrap().clone());
926            let cy_syn = syntax_nodes::Expression::from(cy_node.as_node().unwrap().clone());
927            let cx =
928                Box::new(Expression::from_expression_node(cx_syn.clone(), ctx).maybe_convert_to(
929                    Type::LogicalLength,
930                    &cx_syn,
931                    ctx.diag,
932                    &ctx.symbol_counters,
933                ));
934            let cy =
935                Box::new(Expression::from_expression_node(cy_syn.clone(), ctx).maybe_convert_to(
936                    Type::LogicalLength,
937                    &cy_syn,
938                    ctx.diag,
939                    &ctx.symbol_counters,
940                ));
941            Some((cx, cy))
942        };
943
944        let (grad_kind, stops_start_idx) = if grad_text.starts_with("linear") {
945            let angle_expr = match all_subs.first() {
946                Some(e) if e.kind() == SyntaxKind::Expression => {
947                    syntax_nodes::Expression::from(e.as_node().unwrap().clone())
948                }
949                _ => {
950                    ctx.diag.push_error("Expected angle expression".into(), &node);
951                    return Expression::Invalid;
952                }
953            };
954            if all_subs.get(1).is_none_or(|s| s.kind() != SyntaxKind::Comma) {
955                ctx.diag.push_error(
956                    "Angle expression must be an angle followed by a comma".into(),
957                    &node,
958                );
959                return Expression::Invalid;
960            }
961            let angle = Box::new(
962                Expression::from_expression_node(angle_expr.clone(), ctx).maybe_convert_to(
963                    Type::Angle,
964                    &angle_expr,
965                    ctx.diag,
966                    &ctx.symbol_counters,
967                ),
968            );
969            (GradKind::Linear { angle }, 2)
970        } else if grad_text.starts_with("radial") {
971            if !all_subs.first().is_some_and(|n| {
972                matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "circle")
973            }) {
974                ctx.diag.push_error("Expected 'circle': currently, only @radial-gradient(circle, ...) are supported".into(), &node);
975                return Expression::Invalid;
976            }
977            // CSS syntax: `circle [<radius>] [at <x> <y>]` — radius before center, no keyword.
978            let mut idx = 1;
979
980            // Parse optional radius (a length expression that is not the "at" keyword).
981            // Only consume the node when it actually resolves to a length-compatible type;
982            // a colour keyword like `blue` must not silently become a failed conversion.
983            let radius = if all_subs.get(idx).is_some_and(|n| {
984                n.kind() == SyntaxKind::Expression
985                    && !matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "at")
986            }) {
987                let r = all_subs.get(idx).unwrap();
988                let r_syn = syntax_nodes::Expression::from(r.as_node().unwrap().clone());
989                let expr = Expression::from_expression_node(r_syn.clone(), ctx);
990                if matches!(expr.ty(), Type::LogicalLength | Type::Float32 | Type::Int32) {
991                    let radius = Box::new(
992                        expr.maybe_convert_to(Type::LogicalLength, &r_syn, ctx.diag, &ctx.symbol_counters),
993                    );
994                    idx += 1;
995                    Some(radius)
996                } else {
997                    None
998                }
999            } else {
1000                None
1001            };
1002
1003            // Parse optional "at <x> <y>".
1004            let center = if all_subs.get(idx).is_some_and(
1005                |n| matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "at"),
1006            ) {
1007                let center = parse_at_center(idx + 1, ctx);
1008                if center.is_none() {
1009                    ctx.diag.push_error(
1010                        "Expected two length values after 'at'".into(),
1011                        all_subs.get(idx).unwrap(),
1012                    );
1013                    return Expression::Invalid;
1014                }
1015                idx += 3; // consumed "at x y"
1016                center
1017            } else {
1018                None
1019            };
1020
1021            let stops_start = if all_subs.get(idx).is_none() {
1022                idx
1023            } else if all_subs.get(idx).is_some_and(|s| s.kind() == SyntaxKind::Comma) {
1024                idx + 1
1025            } else {
1026                if idx == 1 {
1027                    let message = "'circle' must be followed by a comma, a radius, or 'at'".into();
1028                    if let Some(error_node) = all_subs.get(idx) {
1029                        ctx.diag.push_error(message, error_node);
1030                    } else {
1031                        ctx.diag.push_error(message, &node);
1032                    }
1033                } else {
1034                    ctx.diag
1035                        .push_error("gradient header must be followed by a comma".into(), &node);
1036                }
1037                return Expression::Invalid;
1038            };
1039            (GradKind::Radial { center, radius }, stops_start)
1040        } else if grad_text.starts_with("conic") {
1041            // Parse optional "from <angle>" and/or "at <x> <y>" before the comma
1042            let mut idx = 0usize;
1043            let from_angle = if all_subs.first().is_some_and(|n| {
1044                matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "from")
1045            }) {
1046                // Parse "from <angle>" syntax
1047                let angle_expr = match all_subs.get(1) {
1048                    Some(e) if e.kind() == SyntaxKind::Expression => {
1049                        syntax_nodes::Expression::from(e.as_node().unwrap().clone())
1050                    }
1051                    _ => {
1052                        ctx.diag.push_error("Expected angle expression after 'from'".into(), &node);
1053                        return Expression::Invalid;
1054                    }
1055                };
1056                let angle = Box::new(
1057                    Expression::from_expression_node(angle_expr.clone(), ctx).maybe_convert_to(
1058                        Type::Angle,
1059                        &angle_expr,
1060                        ctx.diag, &ctx.symbol_counters),
1061                );
1062                idx = 2; // consumed "from" and angle
1063                angle
1064            } else {
1065                // Default to 0deg when "from" is omitted
1066                Box::new(Expression::NumberLiteral(0., Unit::Deg))
1067            };
1068
1069            // Parse optional "at <x> <y>" after the optional "from <angle>"
1070            let center = if all_subs.get(idx).is_some_and(
1071                |n| matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "at"),
1072            ) {
1073                let center = parse_at_center(idx + 1, ctx);
1074                if center.is_none() {
1075                    ctx.diag.push_error(
1076                        "Expected two length values after 'at'".into(),
1077                        all_subs.get(idx).unwrap(),
1078                    );
1079                    return Expression::Invalid;
1080                }
1081                idx += 3; // consumed "at", x, y
1082                center
1083            } else {
1084                None
1085            };
1086
1087            // Expect a comma after the header (if any header elements were present)
1088            if (idx > 0) && all_subs.get(idx).is_none_or(|s| s.kind() != SyntaxKind::Comma) {
1089                ctx.diag.push_error("gradient header must be followed by a comma".into(), &node);
1090                return Expression::Invalid;
1091            }
1092            let stops_start = if idx > 0 { idx + 1 } else { 0 };
1093            (GradKind::Conic { from_angle, center }, stops_start)
1094        } else {
1095            // Parser should have ensured we have one of the linear, radial or conic gradient
1096            panic!("Not a gradient {grad_text:?}");
1097        };
1098
1099        let mut stops = Vec::new();
1100        enum Stop {
1101            Empty,
1102            Color(Expression),
1103            Finished,
1104        }
1105        let mut current_stop = Stop::Empty;
1106        for n in all_subs.iter().skip(stops_start_idx) {
1107            if n.kind() == SyntaxKind::Comma {
1108                match std::mem::replace(&mut current_stop, Stop::Empty) {
1109                    Stop::Empty => {
1110                        ctx.diag.push_error("Expected expression".into(), n);
1111                        break;
1112                    }
1113                    Stop::Finished => {}
1114                    Stop::Color(col) => stops.push((
1115                        col,
1116                        if stops.is_empty() {
1117                            Expression::NumberLiteral(0., Unit::None)
1118                        } else {
1119                            Expression::Invalid
1120                        },
1121                    )),
1122                }
1123            } else {
1124                // To facilitate color literal conversion, adjust the expected type.
1125                let e = ctx.with_expected_type(Type::Color, |ctx| {
1126                    Expression::from_expression_node(n.as_node().unwrap().clone().into(), ctx)
1127                });
1128                match std::mem::replace(&mut current_stop, Stop::Finished) {
1129                    Stop::Empty => {
1130                        current_stop = Stop::Color(e.maybe_convert_to(
1131                            Type::Color,
1132                            n,
1133                            ctx.diag,
1134                            &ctx.symbol_counters,
1135                        ))
1136                    }
1137                    Stop::Finished => {
1138                        ctx.diag.push_error("Expected comma".into(), n);
1139                        break;
1140                    }
1141                    Stop::Color(col) => {
1142                        let stop_type = match &grad_kind {
1143                            GradKind::Conic { .. } => Type::Angle,
1144                            _ => Type::Float32,
1145                        };
1146                        stops.push((
1147                            col,
1148                            e.maybe_convert_to(stop_type, n, ctx.diag, &ctx.symbol_counters),
1149                        ))
1150                    }
1151                }
1152            }
1153        }
1154        match current_stop {
1155            Stop::Color(col) => stops.push((col, Expression::NumberLiteral(1., Unit::None))),
1156            Stop::Empty => {
1157                if let Some((_, e @ Expression::Invalid)) = stops.last_mut() {
1158                    *e = Expression::NumberLiteral(1., Unit::None)
1159                }
1160            }
1161            Stop::Finished => (),
1162        };
1163
1164        // Fix the stop so each has a position.
1165        let mut start = 0;
1166        while start < stops.len() {
1167            start += match stops[start..].iter().position(|s| matches!(s.1, Expression::Invalid)) {
1168                Some(p) => p,
1169                None => break,
1170            };
1171            let (before, rest) = stops.split_at_mut(start);
1172            let pos =
1173                rest.iter().position(|s| !matches!(s.1, Expression::Invalid)).unwrap_or(rest.len());
1174            if pos > 0 && pos < rest.len() {
1175                let (middle, after) = rest.split_at_mut(pos);
1176                let begin = before
1177                    .last()
1178                    .map(|s| &s.1)
1179                    .unwrap_or(&Expression::NumberLiteral(1., Unit::None));
1180                let end = &after.first().expect("The last should never be invalid").1;
1181                for (i, (_, e)) in middle.iter_mut().enumerate() {
1182                    debug_assert!(matches!(e, Expression::Invalid));
1183                    // e = begin + (i+1) * (end - begin) / (pos+1)
1184                    *e = Expression::BinaryExpression {
1185                        lhs: Box::new(begin.clone()),
1186                        rhs: Box::new(Expression::BinaryExpression {
1187                            source_location: None,
1188                            lhs: Box::new(Expression::BinaryExpression {
1189                                source_location: None,
1190                                lhs: Box::new(Expression::NumberLiteral(i as f64 + 1., Unit::None)),
1191                                rhs: Box::new(Expression::BinaryExpression {
1192                                    source_location: None,
1193                                    lhs: Box::new(end.clone()),
1194                                    rhs: Box::new(begin.clone()),
1195                                    op: '-',
1196                                }),
1197                                op: '*',
1198                            }),
1199                            rhs: Box::new(Expression::NumberLiteral(pos as f64 + 1., Unit::None)),
1200                            op: '/',
1201                        }),
1202                        op: '+',
1203                        source_location: None,
1204                    };
1205                }
1206            }
1207            start += pos + 1;
1208        }
1209
1210        match grad_kind {
1211            GradKind::Linear { angle } => Expression::LinearGradient { angle, stops },
1212            GradKind::Radial { center, radius } => {
1213                Expression::RadialGradient { center, radius, stops }
1214            }
1215            GradKind::Conic { from_angle, center } => {
1216                // Normalize stop angles to 0-1 range by dividing by 360deg
1217                let normalized_stops = stops
1218                    .into_iter()
1219                    .map(|(color, angle_expr)| {
1220                        let angle_typed = angle_expr.maybe_convert_to(
1221                            Type::Angle,
1222                            &node,
1223                            ctx.diag,
1224                            &ctx.symbol_counters,
1225                        );
1226                        let normalized_pos = Expression::BinaryExpression {
1227                            lhs: Box::new(angle_typed),
1228                            rhs: Box::new(Expression::NumberLiteral(360., Unit::Deg)),
1229                            op: '/',
1230                            source_location: None,
1231                        };
1232                        (color, normalized_pos)
1233                    })
1234                    .collect();
1235
1236                // Convert from_angle to degrees (don't normalize to 0-1)
1237                let from_angle_degrees =
1238                    from_angle.maybe_convert_to(Type::Angle, &node, ctx.diag, &ctx.symbol_counters);
1239
1240                Expression::ConicGradient {
1241                    from_angle: Box::new(from_angle_degrees),
1242                    center,
1243                    stops: normalized_stops,
1244                }
1245            }
1246        }
1247    }
1248
1249    fn from_at_markdown(node: syntax_nodes::AtMarkdown, ctx: &mut LookupCtx) -> Expression {
1250        let mut raw_exprs: Vec<(Expression, crate::parser::SyntaxNode)> = Vec::new();
1251        let mut source_map = crate::literals::StringLiteralSourceMap::new();
1252        use i_slint_common::styled_text::MARKDOWN_INTERPOLATION_PLACEHOLDER as PLACEHOLDER;
1253
1254        let push_and_check =
1255            |token: &crate::parser::SyntaxToken,
1256             source_map: &mut crate::literals::StringLiteralSourceMap,
1257             diag: &mut crate::diagnostics::BuildDiagnostics| {
1258                let before = source_map.as_str().len();
1259                source_map.push(token, diag);
1260                for (offset, _) in source_map.as_str()[before..].match_indices(PLACEHOLDER) {
1261                    source_map.report(
1262                        diag,
1263                        "\\u{e541} is reserved for @markdown interpolation".into(),
1264                        (before + offset)..(before + offset + PLACEHOLDER.len_utf8()),
1265                        &node,
1266                    );
1267                }
1268            };
1269
1270        for n in node.children_with_tokens() {
1271            if n.kind() == SyntaxKind::StringLiteral {
1272                push_and_check(n.as_token().unwrap(), &mut source_map, ctx.diag);
1273            } else if n.kind() == SyntaxKind::StringTemplate {
1274                for n in n.as_node().unwrap().children_with_tokens() {
1275                    if n.kind() == SyntaxKind::StringLiteral {
1276                        push_and_check(n.as_token().unwrap(), &mut source_map, ctx.diag);
1277                    } else if n.kind() == SyntaxKind::Expression {
1278                        let expr_node = n.into_node().unwrap();
1279                        let expr = Expression::from_expression_node(expr_node.clone().into(), ctx);
1280                        source_map.push_raw_char(PLACEHOLDER, expr_node.to_source_location());
1281                        raw_exprs.push((expr, expr_node));
1282                    }
1283                }
1284            }
1285        }
1286
1287        let markdown = source_map.as_str();
1288        let placeholder_positions: Vec<usize> =
1289            markdown.match_indices(PLACEHOLDER).map(|(pos, _)| pos).collect();
1290
1291        // Replace each placeholder with an ASCII string of the same byte length
1292        // and re-parse.
1293        // pulldown_cmark treats `<zzz>` as inline HTML (unlike the private-use char),
1294        // so errors reveal interpolations inside HTML tag structure.
1295        const PROBE: &str = "zzz";
1296        const _: () = assert!(PROBE.len() == PLACEHOLDER.len_utf8());
1297        let probe = markdown.replace(PLACEHOLDER, PROBE);
1298
1299        let (_, parse_errors) = i_slint_common::styled_text::parse_interpolated::<
1300            &[i_slint_common::styled_text::StyledTextParagraph],
1301        >(&probe, &[]);
1302
1303        let mut color_indices = std::collections::BTreeSet::new();
1304
1305        for e in &parse_errors {
1306            let placeholders_in_range = |r: &core::ops::Range<usize>| -> Vec<usize> {
1307                placeholder_positions
1308                    .iter()
1309                    .enumerate()
1310                    .filter(|(_, pos)| **pos >= r.start && **pos < r.end)
1311                    .map(|(idx, _)| idx)
1312                    .collect()
1313            };
1314
1315            if let Some(r) = e.range() {
1316                let hits = placeholders_in_range(&r);
1317
1318                // InvalidColor("zzz") at a placeholder position →
1319                // this interpolation is a color attribute value.
1320                if i_slint_common::styled_text::invalid_color_value(e) == Some(PROBE)
1321                    && !hits.is_empty()
1322                {
1323                    color_indices.extend(hits);
1324                    continue;
1325                }
1326
1327                // Other errors overlapping a placeholder mean interpolation
1328                // inside HTML tag structure.
1329                if !hits.is_empty() {
1330                    source_map.report(
1331                        ctx.diag,
1332                        "Interpolation (`\\{}`) is not allowed inside HTML tags".into(),
1333                        r,
1334                        &node,
1335                    );
1336                } else {
1337                    source_map.report(ctx.diag, e.to_string(), r, &node);
1338                }
1339            } else {
1340                ctx.diag.push_error(e.to_string(), &node);
1341            }
1342        }
1343
1344        let values = raw_exprs
1345            .into_iter()
1346            .enumerate()
1347            .map(|(idx, (expr, expr_node))| {
1348                if color_indices.contains(&idx) {
1349                    // Color placeholder: require Color type
1350                    Expression::FunctionCall {
1351                        function: BuiltinFunction::ColorToStyledText.into(),
1352                        arguments: vec![expr.maybe_convert_to(
1353                            Type::Color,
1354                            &expr_node,
1355                            ctx.diag,
1356                            &ctx.symbol_counters,
1357                        )],
1358                        source_location: Some(expr_node.to_source_location()),
1359                    }
1360                } else if expr.ty() == Type::StyledText {
1361                    expr
1362                } else {
1363                    Expression::FunctionCall {
1364                        function: BuiltinFunction::StringToStyledText.into(),
1365                        arguments: vec![expr.maybe_convert_to(
1366                            Type::String,
1367                            &expr_node,
1368                            ctx.diag,
1369                            &ctx.symbol_counters,
1370                        )],
1371                        source_location: Some(expr_node.to_source_location()),
1372                    }
1373                }
1374            })
1375            .collect();
1376
1377        Expression::FunctionCall {
1378            function: BuiltinFunction::ParseMarkdown.into(),
1379            arguments: vec![
1380                Expression::StringLiteral(source_map.into_string().into()),
1381                Expression::Array { element_ty: Type::StyledText, values },
1382            ],
1383            source_location: Some(node.to_source_location()),
1384        }
1385    }
1386
1387    fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression {
1388        let mut source_map = crate::literals::StringLiteralSourceMap::new();
1389        let Some(string_token) = node.child_token(SyntaxKind::StringLiteral) else {
1390            ctx.diag.push_error("Cannot parse string literal".into(), &node);
1391            return Expression::Invalid;
1392        };
1393        if !source_map.push(&string_token, ctx.diag) {
1394            return Expression::Invalid;
1395        }
1396        let string: SmolStr = source_map.as_str().into();
1397        let context = node.TrContext().map(|n| {
1398            crate::literals::unescape_string_reporting(
1399                n.child_token(SyntaxKind::StringLiteral).as_ref(),
1400                ctx.diag,
1401                &n,
1402            )
1403            .unwrap_or_default()
1404        });
1405        let plural = node.TrPlural().map(|pl| {
1406            let s = crate::literals::unescape_string_reporting(
1407                pl.child_token(SyntaxKind::StringLiteral).as_ref(),
1408                ctx.diag,
1409                &pl,
1410            )
1411            .unwrap_or_default();
1412            let n = pl.Expression();
1413            let expr = Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
1414                Type::Int32,
1415                &n,
1416                ctx.diag,
1417                &ctx.symbol_counters,
1418            );
1419            (s, expr)
1420        });
1421
1422        let domain = ctx
1423            .type_loader
1424            .and_then(|tl| tl.compiler_config.translation_domain.clone())
1425            .unwrap_or_default();
1426
1427        let subs = node.Expression().map(|n| {
1428            Expression::from_expression_node(n.clone(), ctx).maybe_convert_to(
1429                Type::String,
1430                &n,
1431                ctx.diag,
1432                &ctx.symbol_counters,
1433            )
1434        });
1435        let values = subs.collect::<Vec<_>>();
1436
1437        // check format string
1438        {
1439            let mut arg_idx = 0;
1440            let mut pos_max = 0;
1441            let mut pos = 0;
1442            let mut has_n = false;
1443            while let Some(mut p) = string[pos..].find(['{', '}']) {
1444                if string.len() - pos < p + 1 {
1445                    p += pos;
1446                    source_map.report(
1447                        ctx.diag,
1448                        "Unescaped trailing '{' in format string. Escape '{' with '{{'".into(),
1449                        p..p + 1,
1450                        &node,
1451                    );
1452                    break;
1453                }
1454                p += pos;
1455
1456                // Skip escaped }
1457                if string.get(p..=p) == Some("}") {
1458                    if string.get(p + 1..=p + 1) == Some("}") {
1459                        pos = p + 2;
1460                        continue;
1461                    } else {
1462                        source_map.report(
1463                            ctx.diag,
1464                            "Unescaped '}' in format string. Escape '}' with '}}'".into(),
1465                            p..p + 1,
1466                            &node,
1467                        );
1468                        break;
1469                    }
1470                }
1471
1472                // Skip escaped {
1473                if string.get(p + 1..=p + 1) == Some("{") {
1474                    pos = p + 2;
1475                    continue;
1476                }
1477
1478                // Find the argument
1479                let end = if let Some(end) = string[p..].find('}') {
1480                    end + p
1481                } else {
1482                    source_map.report(
1483                        ctx.diag,
1484                        "Unterminated placeholder in format string. '{' must be escaped with '{{'"
1485                            .into(),
1486                        p..string.len(),
1487                        &node,
1488                    );
1489                    break;
1490                };
1491                let argument = &string[p + 1..end];
1492                if argument.is_empty() {
1493                    arg_idx += 1;
1494                } else if let Ok(n) = argument.parse::<u16>() {
1495                    pos_max = pos_max.max(n as usize + 1);
1496                } else if argument == "n" {
1497                    has_n = true;
1498                    if plural.is_none() {
1499                        source_map.report(
1500                            ctx.diag,
1501                            "`{n}` placeholder can only be found in plural form".into(),
1502                            p..end + 1,
1503                            &node,
1504                        );
1505                    }
1506                } else {
1507                    source_map.report(
1508                        ctx.diag,
1509                        "Invalid '{...}' placeholder in format string. The placeholder must be a number, or braces must be escaped with '{{' and '}}'".into(),
1510                        p..end + 1,
1511                        &node,
1512                    );
1513                    break;
1514                };
1515                pos = end + 1;
1516            }
1517            if arg_idx > 0 && pos_max > 0 {
1518                ctx.diag.push_error(
1519                    "Cannot mix positional and non-positional placeholder in format string".into(),
1520                    &node,
1521                );
1522            } else if arg_idx > values.len() || pos_max > values.len() {
1523                let num = arg_idx.max(pos_max);
1524                let note = if !has_n && plural.is_some() {
1525                    ". Note: use `{n}` for the argument after '%'"
1526                } else {
1527                    ""
1528                };
1529                ctx.diag.push_error(
1530                    format!("Format string contains {num} placeholders, but only {} extra arguments were given{note}", values.len()),
1531                    &node,
1532                );
1533            }
1534        }
1535
1536        let plural =
1537            plural.unwrap_or((SmolStr::default(), Expression::NumberLiteral(1., Unit::None)));
1538
1539        let context = context.or_else(|| {
1540            if !ctx.type_loader.is_some_and(|tl| {
1541                tl.compiler_config.default_translation_context
1542                    == crate::DefaultTranslationContext::None
1543            }) {
1544                // Get the component name as a default
1545                ctx.component_scope
1546                    .first()
1547                    .and_then(|e| e.borrow().enclosing_component.upgrade())
1548                    .map(|c| c.id.clone())
1549            } else {
1550                None
1551            }
1552        });
1553
1554        Expression::FunctionCall {
1555            function: BuiltinFunction::Translate.into(),
1556            arguments: vec![
1557                Expression::StringLiteral(string),
1558                Expression::StringLiteral(context.unwrap_or_default()),
1559                Expression::StringLiteral(domain.into()),
1560                Expression::Array { element_ty: Type::String, values },
1561                plural.1,
1562                Expression::StringLiteral(plural.0),
1563            ],
1564            source_location: Some(node.to_source_location()),
1565        }
1566    }
1567
1568    pub fn from_at_keys_node(node: syntax_nodes::AtKeys, ctx: &mut LookupCtx) -> Self {
1569        let mut keys = langtype::Keys::default();
1570
1571        let mut key_code: Option<(SmolStr, ShiftBehavior, NodeOrToken)> = None;
1572
1573        let idents_and_questions: Vec<_> = node
1574            .children_with_tokens()
1575            .filter(|n| matches!(n.kind(), SyntaxKind::Identifier | SyntaxKind::Question))
1576            // The first identifier is always `keys`
1577            .skip(1)
1578            .collect();
1579
1580        for (index, ident_or_question) in idents_and_questions.iter().enumerate() {
1581            if ident_or_question.kind() == SyntaxKind::Question {
1582                continue;
1583            }
1584            let identifier = ident_or_question;
1585
1586            let is_question = || -> bool {
1587                matches!(
1588                    idents_and_questions.get(index + 1).map(NodeOrToken::kind),
1589                    Some(SyntaxKind::Question)
1590                )
1591            };
1592
1593            match identifier.as_token().unwrap().text() {
1594                "Alt" => {
1595                    if is_question() {
1596                        keys.ignore_alt = true;
1597                    } else {
1598                        keys.modifiers.alt = true;
1599                    }
1600                }
1601                "Control" => keys.modifiers.control = true,
1602                "Meta" => keys.modifiers.meta = true,
1603                "Shift" => {
1604                    if is_question() {
1605                        keys.ignore_shift = true;
1606                    } else {
1607                        keys.modifiers.shift = true;
1608                    }
1609                }
1610                key_name => {
1611                    if let Some((key, shiftbehavior)) = lookup_key_name(key_name) {
1612                        key_code = Some((
1613                            SmolStr::from_iter(core::iter::once(key)),
1614                            shiftbehavior,
1615                            identifier.clone(),
1616                        ))
1617                    } else {
1618                        // TODO: This should suggest more kinds of close matches
1619                        let uppercased = key_name.to_uppercase();
1620                        let hint = if lookup_key_name(&uppercased).is_some() {
1621                            // common case: @keys(Control+a) instead of @keys(Control+A)
1622                            format!("Use uppercase {uppercased} instead")
1623                        } else {
1624                            format!("Consider using \"{key_name}\"")
1625                        };
1626                        ctx.diag.push_error(
1627                            format!("{key_name} not defined in the Keys namespace\n({hint})"),
1628                            identifier,
1629                        );
1630                        keys.modifiers = KeyboardModifiers::default();
1631                        break;
1632                    }
1633                }
1634            }
1635        }
1636
1637        // Handle localization issues regarding shift per-keycode
1638        // This only applies to keys that are in the Key namespace
1639        if let Some((key_code, shift_behavior, node)) = key_code {
1640            match shift_behavior {
1641                ShiftBehavior::LocalizedShiftable { shifted_hint } => {
1642                    if keys.ignore_shift {
1643                        ctx.diag.push_warning(
1644                            format!(
1645                                "{name} already implies Shift? (remove Shift?)",
1646                                name = node.as_token().unwrap().text()
1647                            ),
1648                            &node,
1649                        );
1650                    }
1651                    keys.ignore_shift = true;
1652                    if keys.modifiers.shift {
1653                        let shifted_hint = lookup_key_name(shifted_hint).map(|(shifted_code, _shift_behavior)|
1654                            format!("\nConsider using {shifted_hint} to match when the user types '{shifted_code}'")
1655                        ).unwrap_or_default();
1656
1657                        ctx.diag.push_error(
1658                            format!(
1659                                "{name} implies Shift? to support different keyboard layouts\n\
1660                                Remove Shift to match when the user types '{key_code}'{shifted_hint}",
1661                                name = node.as_token().unwrap().text()
1662                            ),
1663                            &node,
1664                        );
1665                    }
1666                }
1667                // Unshiftable keys ignore the shift state in their key_code
1668                // No special action needed
1669                ShiftBehavior::Unshiftable => {}
1670            }
1671            keys.key = key_code;
1672        }
1673
1674        // If there is a string literal, use it as the key
1675        if let Some(token) = node.child_token(SyntaxKind::StringLiteral)
1676            && let Some(key) =
1677                crate::literals::unescape_string_reporting(Some(&token), ctx.diag, &token)
1678        {
1679            // NFC-normalize the key string for consistent matching
1680            let normalizer = icu_normalizer::ComposingNormalizer::new_nfc();
1681            let key: SmolStr = normalizer.normalize(&key).into();
1682
1683            // Validate that the string literal contains exactly one grapheme cluster
1684            let grapheme_count = key.graphemes(true).count();
1685            if grapheme_count == 0 {
1686                ctx.diag.push_error("Key string literal must not be empty".to_string(), &token);
1687            } else if grapheme_count > 1 {
1688                ctx.diag.push_error(
1689                    format!(
1690                        "Key string literal must contain exactly one grapheme cluster, found {grapheme_count}",
1691                    ),
1692                    &token,
1693                );
1694            }
1695
1696            keys.key = key;
1697
1698            let lowercase: SmolStr = keys.key.to_lowercase().into();
1699            if lowercase != keys.key {
1700                ctx.diag.push_error(
1701                    format!(
1702                        "Key string literals must currently be lowercase, use \"{lowercase}\" instead",
1703                    ),
1704                    &token,
1705                );
1706            }
1707        }
1708
1709        Expression::Keys(keys)
1710    }
1711
1712    /// Perform the lookup
1713    fn from_qualified_name_node(node: syntax_nodes::QualifiedName, ctx: &mut LookupCtx) -> Self {
1714        Self::from_lookup_result(
1715            lookup_qualified_name_node(node.clone(), ctx, LookupPhase::default()),
1716            ctx,
1717            &node,
1718        )
1719    }
1720
1721    fn from_lookup_result(
1722        r: Option<LookupResult>,
1723        ctx: &mut LookupCtx,
1724        node: &dyn Spanned,
1725    ) -> Self {
1726        let Some(r) = r else {
1727            assert!(ctx.diag.has_errors());
1728            return Self::Invalid;
1729        };
1730        match r {
1731            LookupResult::Expression { expression, .. } => expression,
1732            // `spring` used bare (no call parens) is a spring curve with the default bounce of 0.
1733            LookupResult::Callable(LookupResultCallable::Macro(BuiltinMacroFunction::Spring)) => {
1734                Expression::EasingCurve(crate::expression_tree::EasingCurve::Spring(0.))
1735            }
1736            LookupResult::Callable(c) => {
1737                let what = match c {
1738                    LookupResultCallable::Callable(Callable::Callback(..)) => "Callback",
1739                    LookupResultCallable::Callable(Callable::Builtin(..)) => "Builtin function",
1740                    LookupResultCallable::Macro(..) => "Builtin function",
1741                    LookupResultCallable::MemberFunction { .. } => "Member function",
1742                    _ => "Function",
1743                };
1744                ctx.diag
1745                    .push_error(format!("{what} must be called. Did you forgot the '()'?",), node);
1746                Self::Invalid
1747            }
1748            LookupResult::Enumeration(..) => {
1749                ctx.diag.push_error("Cannot take reference to an enum".to_string(), node);
1750                Self::Invalid
1751            }
1752            LookupResult::Namespace(..) => {
1753                ctx.diag.push_error("Cannot take reference to a namespace".to_string(), node);
1754                Self::Invalid
1755            }
1756        }
1757    }
1758
1759    fn from_function_call_node(
1760        node: syntax_nodes::FunctionCallExpression,
1761        ctx: &mut LookupCtx,
1762    ) -> Expression {
1763        let mut arguments = Vec::new();
1764
1765        let mut sub_expr = node.Expression();
1766
1767        let func_expr = sub_expr.next().unwrap();
1768        // The argument list `(...)`, for placing the probe on an empty argument slot.
1769        let args_range = TextRange::new(func_expr.text_range().end(), node.text_range().end());
1770
1771        let (function, source_location) = if let Some(qn) = func_expr.QualifiedName() {
1772            let sl = qn.last_token().unwrap().to_source_location();
1773            (lookup_qualified_name_node(qn, ctx, LookupPhase::default()), sl)
1774        } else if let Some(ma) = func_expr.MemberAccess() {
1775            let base = Self::from_expression_node(ma.Expression(), ctx);
1776            let field = ma.child_token(SyntaxKind::Identifier);
1777            let sl = field.to_source_location();
1778            (maybe_lookup_object(base.into(), field.clone().into_iter(), ctx), sl)
1779        } else {
1780            if Self::from_expression_node(func_expr, ctx).ty() == Type::Invalid {
1781                assert!(ctx.diag.has_errors());
1782            } else {
1783                ctx.diag.push_error("The expression is not a function".into(), &node);
1784            }
1785            return Self::Invalid;
1786        };
1787        // For `.any(predicate)` / `.all(predicate)` / `.find-index(predicate)` the
1788        // closure's argument type is structurally derived from the base array's
1789        // element type. Compute it here so we can hand it to the closure when
1790        // resolving that specific argument.
1791        let expected_closure_arg_type = match &function {
1792            Some(LookupResult::Callable(LookupResultCallable::MemberFunction {
1793                base,
1794                member,
1795                ..
1796            })) if matches!(
1797                **member,
1798                LookupResultCallable::Callable(Callable::Builtin(
1799                    BuiltinFunction::ArrayAny
1800                        | BuiltinFunction::ArrayAll
1801                        | BuiltinFunction::ArrayFindIndex
1802                ))
1803            ) =>
1804            {
1805                let Type::Array(elem_ty) = base.ty() else { unreachable!() };
1806                Some((*elem_ty).clone())
1807            }
1808            _ => None,
1809        };
1810
1811        // Convert the arguments once the parameter types are known, so type-directed
1812        // literals resolve against the parameter type at their exact argument position.
1813        let arg_nodes = sub_expr.collect::<Vec<_>>();
1814        let convert_args = |ctx: &mut LookupCtx, expected: &[Type]| {
1815            // Empty trailing-comma argument has no node: record its parameter type for the probe.
1816            if let Some(offset) = ctx.expected_type_probe_offset() {
1817                let idx = arg_nodes.iter().take_while(|n| n.text_range().end() <= offset).count();
1818                if let Some(ty) = expected.get(idx).cloned() {
1819                    ctx.record_expected_type_probe(args_range, &ty);
1820                }
1821            }
1822            arg_nodes
1823                .iter()
1824                .enumerate()
1825                .map(|(i, n)| {
1826                    let ty = expected.get(i).cloned().unwrap_or(Type::Invalid);
1827                    let expression = ctx.with_expected_type(ty, |ctx| {
1828                        Self::from_argument_expression_node(
1829                            (*n).clone(),
1830                            ctx,
1831                            &expected_closure_arg_type,
1832                        )
1833                    });
1834                    (expression, Some(NodeOrToken::from((**n).clone())))
1835                })
1836                .collect::<Vec<_>>()
1837        };
1838
1839        let Some(function) = function else {
1840            // Check sub-expressions anyway.
1841            convert_args(ctx, &[]);
1842            assert!(ctx.diag.has_errors());
1843            return Self::Invalid;
1844        };
1845        let LookupResult::Callable(function) = function else {
1846            // Check sub-expressions anyway.
1847            convert_args(ctx, &[]);
1848            ctx.diag.push_error("The expression is not a function".into(), &node);
1849            return Self::Invalid;
1850        };
1851
1852        let mut adjust_arg_count = 0;
1853        let function = match function {
1854            LookupResultCallable::Callable(c) => c,
1855            LookupResultCallable::Macro(mac) => {
1856                arguments.extend(convert_args(ctx, &[]));
1857                return crate::builtin_macros::lower_macro(
1858                    mac,
1859                    &source_location,
1860                    arguments.into_iter(),
1861                    ctx.diag,
1862                    &ctx.symbol_counters,
1863                );
1864            }
1865            LookupResultCallable::MemberFunction { member, base, source_node } => {
1866                arguments.push((base, source_node));
1867                adjust_arg_count = 1;
1868                match *member {
1869                    LookupResultCallable::Callable(c) => c,
1870                    LookupResultCallable::Macro(mac) => {
1871                        arguments.extend(convert_args(ctx, &[]));
1872                        return crate::builtin_macros::lower_macro(
1873                            mac,
1874                            &source_location,
1875                            arguments.into_iter(),
1876                            ctx.diag,
1877                            &ctx.symbol_counters,
1878                        );
1879                    }
1880                    LookupResultCallable::MemberFunction { .. } => {
1881                        unreachable!()
1882                    }
1883                }
1884            }
1885        };
1886
1887        match function.ty() {
1888            Type::Function(f) | Type::Callback(f) => {
1889                arguments.extend(convert_args(ctx, f.args.get(adjust_arg_count..).unwrap_or(&[])));
1890            }
1891            _ => arguments.extend(convert_args(ctx, &[])),
1892        }
1893
1894        if matches!(&function, Callable::Callback(nr) if nr.name() == "init") {
1895            ctx.diag.push_warning(
1896                "Calling 'init' explicitly does nothing and is deprecated".into(),
1897                &node,
1898            );
1899        }
1900
1901        let arguments = match function.ty() {
1902            Type::Function(function) | Type::Callback(function) => {
1903                if arguments.len() != function.args.len() {
1904                    ctx.diag.push_error(
1905                        format!(
1906                            "The callback or function expects {} arguments, but {} are provided",
1907                            function.args.len() - adjust_arg_count,
1908                            arguments.len() - adjust_arg_count,
1909                        ),
1910                        &node,
1911                    );
1912                    arguments.into_iter().map(|x| x.0).collect()
1913                } else {
1914                    arguments
1915                        .into_iter()
1916                        .zip(function.args.iter())
1917                        .map(|((e, node), ty)| {
1918                            e.maybe_convert_to(ty.clone(), &node, ctx.diag, &ctx.symbol_counters)
1919                        })
1920                        .collect()
1921                }
1922            }
1923            Type::Invalid => {
1924                debug_assert!(ctx.diag.has_errors(), "The error must already have been reported.");
1925                arguments.into_iter().map(|x| x.0).collect()
1926            }
1927            _ => {
1928                ctx.diag.push_error("The expression is not a function".into(), &node);
1929                arguments.into_iter().map(|x| x.0).collect()
1930            }
1931        };
1932
1933        Expression::FunctionCall { function, arguments, source_location: Some(source_location) }
1934    }
1935
1936    fn from_member_access_node(
1937        node: syntax_nodes::MemberAccess,
1938        ctx: &mut LookupCtx,
1939    ) -> Expression {
1940        let base = Self::from_expression_node(node.Expression(), ctx);
1941        let field = node.child_token(SyntaxKind::Identifier);
1942        Self::from_lookup_result(
1943            maybe_lookup_object(base.into(), field.clone().into_iter(), ctx),
1944            ctx,
1945            &field,
1946        )
1947    }
1948
1949    fn from_self_assignment_node(
1950        node: syntax_nodes::SelfAssignment,
1951        ctx: &mut LookupCtx,
1952    ) -> Expression {
1953        let (lhs_n, rhs_n) = node.Expression();
1954        let mut lhs = Self::from_expression_node(lhs_n.clone(), ctx);
1955        let op = node
1956            .children_with_tokens()
1957            .find_map(|n| match n.kind() {
1958                SyntaxKind::PlusEqual => Some('+'),
1959                SyntaxKind::MinusEqual => Some('-'),
1960                SyntaxKind::StarEqual => Some('*'),
1961                SyntaxKind::DivEqual => Some('/'),
1962                SyntaxKind::Equal => Some('='),
1963                _ => None,
1964            })
1965            .unwrap_or('_');
1966        if lhs.ty() != Type::Invalid {
1967            lhs.try_set_rw(ctx, if op == '=' { "Assignment" } else { "Self assignment" }, &node);
1968        }
1969        let ty = lhs.ty();
1970        let expected_ty = match op {
1971            '=' => ty,
1972            '+' if ty == Type::String || ty.as_unit_product().is_some() => ty,
1973            '-' if ty.as_unit_product().is_some() => ty,
1974            '/' | '*' if ty.as_unit_product().is_some() => Type::Float32,
1975            _ => {
1976                if ty != Type::Invalid {
1977                    ctx.diag.push_error(
1978                        format!("the {op}= operation cannot be done on a {ty}"),
1979                        &lhs_n,
1980                    );
1981                }
1982                Type::Invalid
1983            }
1984        };
1985        let rhs = ctx.with_expected_type(expected_ty.clone(), |ctx| {
1986            Self::from_expression_node(rhs_n.clone(), ctx)
1987        });
1988        Expression::SelfAssignment {
1989            lhs: Box::new(lhs),
1990            rhs: Box::new(rhs.maybe_convert_to(
1991                expected_ty,
1992                &rhs_n,
1993                ctx.diag,
1994                &ctx.symbol_counters,
1995            )),
1996            op,
1997            node: Some(NodeOrToken::Node(node.into())),
1998        }
1999    }
2000
2001    fn from_binary_expression_node(
2002        node: syntax_nodes::BinaryExpression,
2003        ctx: &mut LookupCtx,
2004    ) -> Expression {
2005        let (op, operator) = node
2006            .children_with_tokens()
2007            .find_map(|n| {
2008                let op = match n.kind() {
2009                    SyntaxKind::Plus => '+',
2010                    SyntaxKind::Minus => '-',
2011                    SyntaxKind::Star => '*',
2012                    SyntaxKind::Div => '/',
2013                    SyntaxKind::LessEqual => '≤',
2014                    SyntaxKind::GreaterEqual => '≥',
2015                    SyntaxKind::LAngle => '<',
2016                    SyntaxKind::RAngle => '>',
2017                    SyntaxKind::EqualEqual => '=',
2018                    SyntaxKind::NotEqual => '!',
2019                    SyntaxKind::AndAnd => '&',
2020                    SyntaxKind::OrOr => '|',
2021                    _ => return None,
2022                };
2023                Some((op, Some(n.to_source_location())))
2024            })
2025            .unwrap_or(('_', None));
2026
2027        // In Slint SC, arithmetic (`+`, `-`, `*`), logical (`&&`, `||`), and
2028        // comparison (`==`, `!=`, `<`, `>`, `<=`, `>=`) are in the subset; `/` is
2029        // not. Operands are checked as they resolve, and a result that leaves the
2030        // subset (a `length * length` unit product) is rejected where it is used.
2031        #[cfg(feature = "slint-sc")]
2032        if op == '/' {
2033            ctx.diag.slint_sc_error("Operator '/'", &node);
2034        }
2035
2036        let op_class = operator_class(op);
2037        let (lhs_n, rhs_n) = node.Expression();
2038        // `&&`/`||` operands are bool; a comparison's rhs takes the lhs type. Setting the
2039        // expected type lets a bare literal resolve (or cleanly fail) at that position.
2040        let lhs = if op_class == OperatorClass::LogicalOp {
2041            ctx.with_expected_type(Type::Bool, |ctx| Self::from_expression_node(lhs_n.clone(), ctx))
2042        } else {
2043            Self::from_expression_node(lhs_n.clone(), ctx)
2044        };
2045        let rhs = match op_class {
2046            OperatorClass::ComparisonOp => ctx
2047                .with_expected_type(lhs.ty(), |ctx| Self::from_expression_node(rhs_n.clone(), ctx)),
2048            OperatorClass::LogicalOp => ctx.with_expected_type(Type::Bool, |ctx| {
2049                Self::from_expression_node(rhs_n.clone(), ctx)
2050            }),
2051            OperatorClass::ArithmeticOp => Self::from_expression_node(rhs_n.clone(), ctx),
2052        };
2053
2054        // The conversion target for each operand; `None` keeps the operand as-is.
2055        // Convert both operands at a single construction site below: in unoptimized
2056        // builds, every `Expression::BinaryExpression { .. }` construction gets its
2057        // own stack slots for the operand temporaries, and this function is part of
2058        // the recursion over nested expressions, where large stack frames make
2059        // deeply nested expressions overflow the stack.
2060        let (lhs_target, rhs_target) = match op_class {
2061            OperatorClass::ComparisonOp => {
2062                let ty =
2063                    Self::common_target_type_for_type_list([lhs.ty(), rhs.ty()].iter().cloned());
2064                if !matches!(op, '=' | '!') && ty.as_unit_product().is_none() && ty != Type::String
2065                {
2066                    ctx.diag.push_error(format!("Values of type {ty} cannot be compared"), &node);
2067                }
2068                (Some(ty.clone()), Some(ty))
2069            }
2070            OperatorClass::LogicalOp => (Some(Type::Bool), Some(Type::Bool)),
2071            OperatorClass::ArithmeticOp => {
2072                let (lhs_ty, rhs_ty) = (lhs.ty(), rhs.ty());
2073                if op == '*' || op == '/' {
2074                    let has_unit = |ty: &Type| {
2075                        matches!(ty, Type::UnitProduct(_)) || ty.default_unit().is_some()
2076                    };
2077                    match (has_unit(&lhs_ty), has_unit(&rhs_ty)) {
2078                        (true, true) => (None, None),
2079                        (true, false) => (None, Some(Type::Float32)),
2080                        (false, true) => (Some(Type::Float32), None),
2081                        (false, false) => (Some(Type::Float32), Some(Type::Float32)),
2082                    }
2083                } else if op == '+' || op == '-' {
2084                    let expected_ty =
2085                        if op == '+' && (lhs_ty == Type::String || rhs_ty == Type::String) {
2086                            Type::String
2087                        } else if lhs_ty.default_unit().is_some() {
2088                            lhs_ty
2089                        } else if rhs_ty.default_unit().is_some() {
2090                            rhs_ty
2091                        } else if matches!(lhs_ty, Type::UnitProduct(_)) {
2092                            lhs_ty
2093                        } else if matches!(rhs_ty, Type::UnitProduct(_)) {
2094                            rhs_ty
2095                        } else {
2096                            Type::Float32
2097                        };
2098                    (Some(expected_ty.clone()), Some(expected_ty))
2099                } else {
2100                    unreachable!()
2101                }
2102            }
2103        };
2104        let lhs = match lhs_target {
2105            Some(ty) => lhs.maybe_convert_to(ty, &lhs_n, ctx.diag, &ctx.symbol_counters),
2106            None => lhs,
2107        };
2108        let rhs = match rhs_target {
2109            Some(ty) => rhs.maybe_convert_to(ty, &rhs_n, ctx.diag, &ctx.symbol_counters),
2110            None => rhs,
2111        };
2112        Expression::BinaryExpression {
2113            lhs: Box::new(lhs),
2114            rhs: Box::new(rhs),
2115            op,
2116            source_location: operator,
2117        }
2118    }
2119
2120    fn from_unaryop_expression_node(
2121        node: syntax_nodes::UnaryOpExpression,
2122        ctx: &mut LookupCtx,
2123    ) -> Expression {
2124        let op = node
2125            .children_with_tokens()
2126            .find_map(|n| match n.kind() {
2127                SyntaxKind::Plus => Some('+'),
2128                SyntaxKind::Minus => Some('-'),
2129                SyntaxKind::Bang => Some('!'),
2130                _ => None,
2131            })
2132            .unwrap_or('_');
2133
2134        let exp_n = node.Expression();
2135        let exp = if op == '!' {
2136            ctx.with_expected_type(Type::Bool, |ctx| Self::from_expression_node(exp_n, ctx))
2137        } else {
2138            Self::from_expression_node(exp_n, ctx)
2139        };
2140
2141        let exp = match op {
2142            '!' => exp.maybe_convert_to(Type::Bool, &node, ctx.diag, &ctx.symbol_counters),
2143            '+' | '-' => {
2144                let ty = exp.ty();
2145                if ty.default_unit().is_none()
2146                    && !matches!(
2147                        ty,
2148                        Type::Int32
2149                            | Type::Float32
2150                            | Type::Percent
2151                            | Type::UnitProduct(..)
2152                            | Type::Invalid
2153                    )
2154                {
2155                    ctx.diag.push_error(format!("Unary '{op}' not supported on {ty}"), &node);
2156                }
2157                exp
2158            }
2159            _ => {
2160                assert!(ctx.diag.has_errors());
2161                exp
2162            }
2163        };
2164
2165        Expression::UnaryOp { sub: Box::new(exp), op }
2166    }
2167
2168    fn from_conditional_expression_node(
2169        node: syntax_nodes::ConditionalExpression,
2170        ctx: &mut LookupCtx,
2171    ) -> Expression {
2172        let (condition_n, true_expr_n, false_expr_n) = node.Expression();
2173        let condition = ctx
2174            .with_expected_type(Type::Bool, |ctx| {
2175                Self::from_expression_node(condition_n.clone(), ctx)
2176            })
2177            .maybe_convert_to(Type::Bool, &condition_n, ctx.diag, &ctx.symbol_counters);
2178        let true_expr = Self::from_expression_node(true_expr_n.clone(), ctx);
2179        let false_expr = Self::from_expression_node(false_expr_n.clone(), ctx);
2180        let result_ty = common_expression_type(&true_expr, &false_expr);
2181        let true_expr = true_expr.maybe_convert_to(
2182            result_ty.clone(),
2183            &true_expr_n,
2184            ctx.diag,
2185            &ctx.symbol_counters,
2186        );
2187        let false_expr =
2188            false_expr.maybe_convert_to(result_ty, &false_expr_n, ctx.diag, &ctx.symbol_counters);
2189        Expression::Condition {
2190            condition: Box::new(condition),
2191            true_expr: Box::new(true_expr),
2192            false_expr: Box::new(false_expr),
2193            source_location: node
2194                .child_token(SyntaxKind::Question)
2195                .map(|t| ConditionLocation::Question(t.to_source_location())),
2196        }
2197    }
2198
2199    fn from_index_expression_node(
2200        node: syntax_nodes::IndexExpression,
2201        ctx: &mut LookupCtx,
2202    ) -> Expression {
2203        let (array_expr_n, index_expr_n) = node.Expression();
2204        let array_expr = Self::from_expression_node(array_expr_n, ctx);
2205        let index_expr = ctx
2206            .with_expected_type(Type::Int32, |ctx| {
2207                Self::from_expression_node(index_expr_n.clone(), ctx)
2208            })
2209            .maybe_convert_to(Type::Int32, &index_expr_n, ctx.diag, &ctx.symbol_counters);
2210
2211        let ty = array_expr.ty();
2212        if !matches!(ty, Type::Array(_) | Type::Invalid | Type::Function(_) | Type::Callback(_)) {
2213            ctx.diag.push_error(format!("{ty} is not an indexable type"), &node);
2214        }
2215        Expression::ArrayIndex { array: Box::new(array_expr), index: Box::new(index_expr) }
2216    }
2217
2218    fn from_object_literal_node(
2219        node: syntax_nodes::ObjectLiteral,
2220        ctx: &mut LookupCtx,
2221    ) -> Expression {
2222        let values: BTreeMap<SmolStr, Expression> = node
2223            .ObjectMember()
2224            .map(|n| {
2225                let name = identifier_text(&n).unwrap_or_default();
2226                let field_ty = match &ctx.expected_type {
2227                    Type::Struct(s) => s.fields.get(&name).cloned().unwrap_or_default(),
2228                    _ => Type::Invalid,
2229                };
2230                let value = ctx.with_expected_type(field_ty, |ctx| {
2231                    Expression::from_expression_node(n.Expression(), ctx)
2232                });
2233                (name, value)
2234            })
2235            .collect();
2236        let ty = Arc::new(Struct::new(
2237            values.iter().map(|(k, v)| (k.clone(), v.ty())).collect(),
2238            StructName::None,
2239        ));
2240        Expression::Struct { ty, values }
2241    }
2242
2243    fn from_array_node(node: syntax_nodes::Array, ctx: &mut LookupCtx) -> Expression {
2244        let element_expected = match &ctx.expected_type {
2245            Type::Array(el) => (**el).clone(),
2246            _ => Type::Invalid,
2247        };
2248        // Empty trailing-comma element has no node: record the element type for the probe.
2249        ctx.record_expected_type_probe(node.text_range(), &element_expected);
2250        let mut values: Vec<Expression> = node
2251            .Expression()
2252            .map(|e| {
2253                ctx.with_expected_type(element_expected.clone(), |ctx| {
2254                    Expression::from_expression_node(e, ctx)
2255                })
2256            })
2257            .collect();
2258
2259        let element_ty = if values.is_empty() {
2260            Type::Void
2261        } else {
2262            Self::common_target_type_for_type_list(values.iter().map(|expr| expr.ty()))
2263        };
2264
2265        for e in values.iter_mut() {
2266            *e = core::mem::replace(e, Expression::Invalid).maybe_convert_to(
2267                element_ty.clone(),
2268                &node,
2269                ctx.diag,
2270                &ctx.symbol_counters,
2271            );
2272        }
2273
2274        Expression::Array { element_ty, values }
2275    }
2276
2277    /// Resolve a closure expression. `arg_type` is `Some` only when the closure appears in a
2278    /// position whose callee constrains the argument's type (currently `.any` / `.all`); in
2279    /// that case the body is also required to evaluate to `bool`. When `arg_type` is `None`
2280    /// the closure is still a valid expression of type [`Type::Closure`], but its body cannot
2281    /// be meaningfully typed and any later type-conversion error will be reported at the
2282    /// position that consumes it.
2283    fn from_closure_node(
2284        node: syntax_nodes::Closure,
2285        ctx: &mut LookupCtx,
2286        arg_type: Option<Type>,
2287    ) -> Expression {
2288        if crate::reject_experimental_feature(ctx.diag, ctx.type_register, "closures", &node) {
2289            return Expression::Invalid;
2290        }
2291        let has_expected_arg_type = arg_type.is_some();
2292        let ty = arg_type.unwrap_or(Type::Invalid);
2293        let arg_name = node.DeclaredIdentifier().to_smolstr();
2294        let internal_arg_name: SmolStr = format!("local_{arg_name}").into();
2295
2296        ctx.local_variables.push(vec![(internal_arg_name.clone(), ty)]);
2297        let body_expected_type = if has_expected_arg_type { Type::Bool } else { Type::Invalid };
2298        let expression = ctx.with_expected_type(body_expected_type, |ctx| {
2299            Expression::from_expression_node(node.Expression(), ctx)
2300        });
2301        ctx.local_variables.pop();
2302
2303        let body_ty = expression.ty();
2304        if has_expected_arg_type && body_ty != Type::Bool && body_ty != Type::Invalid {
2305            ctx.diag.push_error(
2306                format!("Closure body must be of type bool, but is {body_ty}"),
2307                &node.Expression(),
2308            );
2309            return Expression::Invalid;
2310        }
2311
2312        Expression::Closure { arg_name: internal_arg_name, expression: Box::new(expression) }
2313    }
2314
2315    /// Resolve a function call argument. If the argument is a closure expression (possibly
2316    /// nested in zero or more parenthesizing `Expression` wrappers), dispatch directly to
2317    /// `from_closure_node` with the expected argument type. Otherwise fall back to the
2318    /// generic expression resolver, in which case any closure encountered inside has no
2319    /// expected argument type.
2320    ///
2321    /// A closure-typed argument that is not written inline (for example a local variable
2322    /// holding a closure) is rejected: the code generators and the interpreter evaluate
2323    /// the closure body directly at the call site, so they require the argument to be a
2324    /// literal [`Expression::Closure`].
2325    fn from_argument_expression_node(
2326        node: syntax_nodes::Expression,
2327        ctx: &mut LookupCtx,
2328        expected_closure_arg_type: &Option<Type>,
2329    ) -> Expression {
2330        if expected_closure_arg_type.is_some() {
2331            let mut current = node.clone();
2332            loop {
2333                let first_meaningful_child = current
2334                    .children()
2335                    .find(|n| matches!(n.kind(), SyntaxKind::Expression | SyntaxKind::Closure));
2336                match first_meaningful_child {
2337                    Some(child) if child.kind() == SyntaxKind::Closure => {
2338                        return Self::from_closure_node(
2339                            child.into(),
2340                            ctx,
2341                            expected_closure_arg_type.clone(),
2342                        );
2343                    }
2344                    Some(child) if child.kind() == SyntaxKind::Expression => {
2345                        current = child.into();
2346                    }
2347                    _ => break,
2348                }
2349            }
2350        }
2351        let expression = Self::from_expression_node(node.clone(), ctx);
2352        if expected_closure_arg_type.is_some()
2353            && expression.ty() == Type::Closure
2354            && !matches!(expression, Expression::Closure { .. })
2355        {
2356            ctx.diag.push_error(
2357                "Closures must be written inline as the argument of 'any', 'all' or 'find-index'"
2358                    .into(),
2359                &node,
2360            );
2361            return Expression::Invalid;
2362        }
2363        expression
2364    }
2365
2366    fn from_string_template_node(
2367        node: syntax_nodes::StringTemplate,
2368        ctx: &mut LookupCtx,
2369    ) -> Expression {
2370        let mut result = None;
2371        for n in node.children_with_tokens() {
2372            let expr = if n.kind() == SyntaxKind::StringLiteral {
2373                let token = n.as_token().unwrap();
2374                crate::literals::unescape_string_reporting(Some(token), ctx.diag, token)
2375                    .map(Self::StringLiteral)
2376                    .unwrap_or(Self::Invalid)
2377            } else if n.kind() == SyntaxKind::Expression {
2378                let node = n.into_node().unwrap();
2379                let expr = Expression::from_expression_node(node.clone().into(), ctx);
2380                expr.maybe_convert_to(Type::String, &node, ctx.diag, &ctx.symbol_counters)
2381            } else {
2382                continue;
2383            };
2384            result = match result {
2385                Some(result) => Some(Expression::BinaryExpression {
2386                    lhs: Box::new(result),
2387                    rhs: Box::new(expr),
2388                    op: '+',
2389                    source_location: None,
2390                }),
2391                None => Some(expr),
2392            }
2393        }
2394        result.unwrap_or_default()
2395    }
2396
2397    /// This function is used to find a type that's suitable for casting each instance of a bunch of expressions
2398    /// to a type that captures most aspects. For example for an array of object literals the result is a merge of
2399    /// all seen fields.
2400    pub fn common_target_type_for_type_list(types: impl Iterator<Item = Type>) -> Type {
2401        types.fold(Type::Invalid, |target_type, expr_ty| {
2402            if target_type == expr_ty {
2403                target_type
2404            } else if target_type == Type::Invalid {
2405                expr_ty
2406            } else {
2407                match (target_type, expr_ty) {
2408                    (Type::Struct(ref result), Type::Struct(ref elem)) => {
2409                        let mut fields = result.fields.clone();
2410                        for (elem_name, elem_ty) in elem.fields.iter() {
2411                            match fields.entry(elem_name.clone()) {
2412                                std::collections::btree_map::Entry::Vacant(free_entry) => {
2413                                    free_entry.insert(elem_ty.clone());
2414                                }
2415                                std::collections::btree_map::Entry::Occupied(
2416                                    mut existing_field,
2417                                ) => {
2418                                    *existing_field.get_mut() =
2419                                        Self::common_target_type_for_type_list(
2420                                            [existing_field.get().clone(), elem_ty.clone()]
2421                                                .into_iter(),
2422                                        );
2423                                }
2424                            }
2425                        }
2426                        // The field defaults must come from the same struct as the name
2427                        let source = if result.name.is_some() { &result } else { &elem };
2428                        Type::Struct(Arc::new(Struct {
2429                            fields,
2430                            field_defaults: source.field_defaults.clone(),
2431                            name: source.name.clone(),
2432                        }))
2433                    }
2434                    (Type::Array(lhs), Type::Array(rhs)) => Type::Array(if *lhs == Type::Void {
2435                        rhs
2436                    } else if *rhs == Type::Void {
2437                        lhs
2438                    } else {
2439                        Self::common_target_type_for_type_list(
2440                            [(*lhs).clone(), (*rhs).clone()].into_iter(),
2441                        )
2442                        .into()
2443                    }),
2444                    (Type::Color, Type::Brush) | (Type::Brush, Type::Color) => Type::Brush,
2445                    (Type::Float32, Type::Int32) | (Type::Int32, Type::Float32) => Type::Float32,
2446                    (target_type, expr_ty) => {
2447                        if expr_ty.can_convert(&target_type) {
2448                            target_type
2449                        } else if target_type.can_convert(&expr_ty)
2450                            || (expr_ty.default_unit().is_some()
2451                                && matches!(target_type, Type::Float32 | Type::Int32))
2452                        {
2453                            // in the or case: The `0` literal.
2454                            expr_ty
2455                        } else {
2456                            // otherwise, use the target type and let further conversion report an error
2457                            target_type
2458                        }
2459                    }
2460                }
2461            }
2462        })
2463    }
2464}
2465
2466use i_slint_common::key_codes::{ShiftBehavior, lookup_key_name};
2467
2468/// Return the type that merge two times when they are used in two branch of a condition
2469///
2470/// Ideally this could just be Expression::common_target_type_for_type_list, but that function
2471/// has a bug actually that it tries to convert things that only works for array literal,
2472/// but doesn't work if we have a type of an array.
2473/// So try to recurse into struct literal and array literal in expression to only call
2474/// common_target_type_for_type_list for them, but always keep the type of the array
2475/// if it is NOT an literal
2476fn common_expression_type(true_expr: &Expression, false_expr: &Expression) -> Type {
2477    fn merge_struct(origin: &Struct, other: &Struct) -> Type {
2478        let mut fields = other.fields.clone();
2479        fields.extend(origin.fields.iter().map(|(k, v)| (k.clone(), v.clone())));
2480        Arc::new(Struct::new(fields, StructName::None)).into()
2481    }
2482
2483    if let Expression::Struct { ty, values } = true_expr {
2484        if let Expression::Struct { values: values2, .. } = false_expr {
2485            let mut fields = BTreeMap::new();
2486            for (k, v) in values.iter() {
2487                if let Some(v2) = values2.get(k) {
2488                    fields.insert(k.clone(), common_expression_type(v, v2));
2489                } else {
2490                    fields.insert(k.clone(), v.ty());
2491                }
2492            }
2493            for (k, v) in values2.iter() {
2494                if !values.contains_key(k) {
2495                    fields.insert(k.clone(), v.ty());
2496                }
2497            }
2498            return Type::Struct(Arc::new(Struct::new(fields, StructName::None)));
2499        } else if let Type::Struct(false_ty) = false_expr.ty() {
2500            return merge_struct(&false_ty, ty);
2501        }
2502    } else if let Expression::Struct { ty, .. } = false_expr
2503        && let Type::Struct(true_ty) = true_expr.ty()
2504    {
2505        return merge_struct(&true_ty, ty);
2506    }
2507
2508    if let Expression::Array { .. } = true_expr {
2509        if let Expression::Array { .. } = false_expr {
2510            // fallback to common_target_type_for_type_list
2511        } else if let Type::Array(ty) = false_expr.ty() {
2512            return Type::Array(ty);
2513        }
2514    } else if let Expression::Array { .. } = false_expr
2515        && let Type::Array(ty) = true_expr.ty()
2516    {
2517        return Type::Array(ty);
2518    }
2519
2520    Expression::common_target_type_for_type_list([true_expr.ty(), false_expr.ty()].into_iter())
2521}
2522
2523/// Perform the lookup
2524fn lookup_qualified_name_node(
2525    node: syntax_nodes::QualifiedName,
2526    ctx: &mut LookupCtx,
2527    phase: LookupPhase,
2528) -> Option<LookupResult> {
2529    let mut it = node
2530        .children_with_tokens()
2531        .filter(|n| n.kind() == SyntaxKind::Identifier)
2532        .filter_map(|n| n.into_token());
2533
2534    let first = if let Some(first) = it.next() {
2535        first
2536    } else {
2537        // There must be at least one member (parser should ensure that)
2538        debug_assert!(ctx.diag.has_errors());
2539        return None;
2540    };
2541
2542    ctx.current_token = Some(first.clone().into());
2543    let first_str = crate::parser::normalize_identifier(first.text());
2544    let global_lookup = crate::lookup::global_lookup();
2545    let result = match global_lookup.lookup(ctx, &first_str) {
2546        None => {
2547            if let Some(slot_element) =
2548                resolve_slot_reference_element(first_str.as_str(), ctx, &node)
2549            {
2550                return continue_lookup_within_element(&slot_element, &mut it, node, ctx);
2551            }
2552            if first_str == "children" || is_declared_slot_in_scope(first_str.as_str(), ctx) {
2553                // resolve_slot_reference_element() already emitted a slot-specific diagnostic.
2554                return None;
2555            }
2556
2557            if let Some(minus_pos) = first.text().find('-') {
2558                // Attempt to recover if the user wanted to write "-" for minus
2559                let first_str = &first.text()[0..minus_pos];
2560                if global_lookup
2561                    .lookup(ctx, &crate::parser::normalize_identifier(first_str))
2562                    .is_some()
2563                {
2564                    ctx.diag.push_error(format!("Unknown unqualified identifier '{}'. Use space before the '-' if you meant a subtraction", first.text()), &node);
2565                    return None;
2566                }
2567            }
2568            for (prefix, e) in
2569                [("self", ctx.component_scope.last()), ("root", ctx.component_scope.first())]
2570            {
2571                if let Some(e) = e
2572                    && e.lookup(ctx, &first_str).is_some()
2573                {
2574                    ctx.diag.push_error(
2575                        format!(
2576                            "Unknown unqualified identifier '{0}'. Did you mean '{prefix}.{0}'?",
2577                            first.text()
2578                        ),
2579                        &node,
2580                    );
2581                    return None;
2582                }
2583            }
2584
2585            if it.next().is_some() {
2586                ctx.diag.push_error(format!("Cannot access id '{}'", first.text()), &node);
2587            } else {
2588                let mut parts = crate::lookup::enum_or_color_suggestions(ctx, &first_str)
2589                    .iter()
2590                    .map(|s| format!("'{s}'"))
2591                    .collect::<Vec<_>>();
2592                let hint = match parts.pop() {
2593                    None => String::new(),
2594                    Some(last) if parts.is_empty() => format!(". Did you mean {last}?"),
2595                    Some(last) => format!(". Did you mean {} or {last}?", parts.join(", ")),
2596                };
2597                ctx.diag.push_error(
2598                    format!("Unknown unqualified identifier '{}'{hint}", first.text()),
2599                    &node,
2600                );
2601            }
2602            return None;
2603        }
2604        Some(x) => x,
2605    };
2606
2607    if let Some(depr) = result.deprecated() {
2608        ctx.diag.push_property_deprecation_warning_with_message(&first_str, depr, &first);
2609    }
2610
2611    match result {
2612        LookupResult::Expression { expression: Expression::ElementReference(e), .. } => {
2613            continue_lookup_within_element(&e.upgrade().unwrap(), &mut it, node, ctx)
2614        }
2615        LookupResult::Expression {
2616            expression: mut e @ Expression::RepeaterModelReference { .. },
2617            ..
2618        } if matches!(phase, LookupPhase::ResolvingTwoWayBindings) => {
2619            // The enclosing model expression may not be resolved yet
2620            // (e.g. when called from `infer_aliases_types`). Skip type
2621            // checking here; `resolve_two_way_binding` does it later.
2622            for n in it {
2623                e = Expression::StructFieldAccess { base: e.into(), name: n.text().into() };
2624            }
2625            Some(e.into())
2626        }
2627        result => maybe_lookup_object(result, it, ctx),
2628    }
2629}
2630
2631fn resolve_slot_reference_element(
2632    name: &str,
2633    ctx: &mut LookupCtx,
2634    node: &dyn Spanned,
2635) -> Option<ElementRc> {
2636    if name == "children" {
2637        ctx.diag.push_error(
2638            "The default slot '@children' cannot be referenced in expressions".into(),
2639            node,
2640        );
2641        return None;
2642    }
2643
2644    for scope_elem in ctx.component_scope.iter().rev() {
2645        let scope_elem_ref = scope_elem.borrow();
2646        let repeated = scope_elem_ref.repeated.is_some();
2647        let mut matches = scope_elem_ref.children.iter().filter(|child| {
2648            child.borrow().slot_target.as_ref().is_some_and(|slot| slot.as_str() == name)
2649        });
2650        if let Some(found) = matches.next() {
2651            if matches.next().is_some() {
2652                ctx.diag.push_error(format!("Duplicate assignment to slot '{name}'"), node);
2653                return None;
2654            }
2655
2656            if repeated {
2657                ctx.diag.push_error(
2658                    format!(
2659                        "Slot '{name}' cannot be referenced inside repeated or conditional elements"
2660                    ),
2661                    node,
2662                );
2663                return None;
2664            }
2665
2666            return Some(found.clone());
2667        }
2668    }
2669
2670    if is_declared_slot_in_scope(name, ctx) {
2671        ctx.diag.push_error(format!("Slot '{name}' is not assigned in this instance"), node);
2672        return None;
2673    }
2674
2675    None
2676}
2677
2678fn is_declared_slot_in_scope(name: &str, ctx: &LookupCtx) -> bool {
2679    ctx.component_scope.iter().rev().any(|scope_elem| {
2680        let scope_elem_ref = scope_elem.borrow();
2681        let ElementType::Component(component) = &scope_elem_ref.base_type else {
2682            return false;
2683        };
2684        component.declared_slots.borrow().iter().any(|slot| slot.name == name)
2685    })
2686}
2687
2688fn continue_lookup_within_element(
2689    elem: &ElementRc,
2690    it: &mut impl Iterator<Item = crate::parser::SyntaxToken>,
2691    node: syntax_nodes::QualifiedName,
2692    ctx: &mut LookupCtx,
2693) -> Option<LookupResult> {
2694    let second = if let Some(second) = it.next() {
2695        second
2696    } else if matches!(ctx.property_type, Type::ElementReference) {
2697        return Some(Expression::ElementReference(Rc::downgrade(elem)).into());
2698    } else {
2699        // Try to recover in case we wanted to access a property
2700        let mut rest = String::new();
2701        if let Some(LookupResult::Expression {
2702            expression: Expression::PropertyReference(nr),
2703            ..
2704        }) = crate::lookup::InScopeLookup.lookup(ctx, &elem.borrow().id)
2705        {
2706            let e = nr.element();
2707            let e_borrowed = e.borrow();
2708            let mut id = e_borrowed.id.as_str();
2709            if id.is_empty() {
2710                if ctx.component_scope.last().is_some_and(|x| Rc::ptr_eq(&e, x)) {
2711                    id = "self";
2712                } else if ctx.component_scope.first().is_some_and(|x| Rc::ptr_eq(&e, x)) {
2713                    id = "root";
2714                } else if ctx.component_scope.iter().nth_back(1).is_some_and(|x| Rc::ptr_eq(&e, x))
2715                {
2716                    id = "parent";
2717                }
2718            };
2719            if !id.is_empty() {
2720                rest =
2721                    format!(". Use '{id}.{}' to access the property with the same name", nr.name());
2722            }
2723        } else if let Some(LookupResult::Expression {
2724            expression: Expression::EnumerationValue(value),
2725            ..
2726        }) = crate::lookup::TypeSpecificLookup.lookup(ctx, &elem.borrow().id)
2727        {
2728            rest = format!(
2729                ". Use '{}.{value}' to access the enumeration value",
2730                value.enumeration.name
2731            );
2732        }
2733        ctx.diag.push_error(format!("Cannot take reference of an element{rest}"), &node);
2734        return None;
2735    };
2736    let prop_name = crate::parser::normalize_identifier(second.text());
2737
2738    let is_local_element = ctx.is_local_element(elem);
2739    let mode = if is_local_element {
2740        PropertyLookupMode::ComponentLocal
2741    } else {
2742        PropertyLookupMode::FromOutside
2743    };
2744    let lookup_result = elem.borrow().lookup_property(&prop_name, mode);
2745    let local_to_component = lookup_result.is_local_to_component && is_local_element;
2746    // A property or function whose type is outside the Slint SC subset
2747    // doesn't resolve; callbacks do, so a handler can invoke them.
2748    let sc_resolves = !ctx.diag.is_slint_sc() || lookup_result.property_type.is_slint_sc();
2749
2750    if sc_resolves && lookup_result.property_type.is_property_type() {
2751        if !local_to_component && lookup_result.property_visibility == PropertyVisibility::Private {
2752            ctx.diag.push_error(format!("The property '{}' is private. Annotate it with 'in', 'out' or 'in-out' to make it accessible from other components", second.text()), &second);
2753            return None;
2754        } else if lookup_result.property_visibility == PropertyVisibility::Fake {
2755            ctx.diag.push_error(
2756                "This special property can only be used to make a binding and cannot be accessed"
2757                    .to_string(),
2758                &second,
2759            );
2760            return None;
2761        } else if lookup_result.resolved_name != prop_name.as_str() {
2762            ctx.diag.push_property_deprecation_warning(
2763                &prop_name,
2764                &lookup_result.resolved_name,
2765                &second,
2766            );
2767        } else if let Some(message) =
2768            lookup_result.deprecated.as_ref().filter(|_| !local_to_component)
2769        {
2770            // `@deprecated` properties only warn when accessed from outside the declaring component
2771            ctx.diag.push_property_deprecation_warning_with_message(&prop_name, message, &second);
2772        } else if let Some(deprecated) =
2773            crate::lookup::check_extra_deprecated(elem, ctx, &prop_name)
2774        {
2775            ctx.diag.push_property_deprecation_warning_with_message(
2776                &prop_name,
2777                &deprecated,
2778                &second,
2779            );
2780        }
2781        let prop = Expression::PropertyReference(NamedReference::new(
2782            elem,
2783            lookup_result.internal_or_resolved_name(),
2784        ));
2785        maybe_lookup_object(prop.into(), it, ctx)
2786    } else if matches!(lookup_result.property_type, Type::Callback { .. }) {
2787        if let Some(message) = lookup_result.deprecated.as_ref().filter(|_| !local_to_component) {
2788            ctx.diag.push_property_deprecation_warning_with_message(&prop_name, message, &second);
2789        }
2790        if let Some(x) = it.next() {
2791            ctx.diag.push_error("Cannot access fields of callback".into(), &x)
2792        }
2793        Some(LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(
2794            NamedReference::new(elem, lookup_result.internal_or_resolved_name()),
2795        ))))
2796    } else if sc_resolves && let Type::Function(fun) = &lookup_result.property_type {
2797        if lookup_result.property_visibility == PropertyVisibility::Private && !local_to_component {
2798            let message = format!(
2799                "The function '{}' is private. Annotate it with 'public' to make it accessible from other components",
2800                second.text()
2801            );
2802            if !lookup_result.is_local_to_component {
2803                ctx.diag.push_error(message, &second);
2804            } else {
2805                ctx.diag.push_warning(message+". Note: this used to be allowed in previous version, but this should be considered an error", &second);
2806            }
2807        } else if lookup_result.property_visibility == PropertyVisibility::Protected
2808            && !local_to_component
2809            && !(lookup_result.is_in_direct_base
2810                && ctx.component_scope.first().is_some_and(|x| Rc::ptr_eq(x, elem)))
2811        {
2812            ctx.diag.push_error(format!("The function '{}' is protected", second.text()), &second);
2813        }
2814        if let Some(message) = lookup_result.deprecated.as_ref().filter(|_| !local_to_component) {
2815            ctx.diag.push_property_deprecation_warning_with_message(&prop_name, message, &second);
2816        }
2817        if let Some(x) = it.next() {
2818            ctx.diag.push_error("Cannot access fields of a function".into(), &x)
2819        }
2820        let callable = match lookup_result.builtin_function {
2821            Some(builtin) => Callable::Builtin(builtin),
2822            None => Callable::Function(NamedReference::new(
2823                elem,
2824                lookup_result.internal_or_resolved_name(),
2825            )),
2826        };
2827        if matches!(fun.args.first(), Some(Type::ElementReference)) {
2828            LookupResult::Callable(LookupResultCallable::MemberFunction {
2829                base: Expression::ElementReference(Rc::downgrade(elem)),
2830                source_node: Some(NodeOrToken::Node(node.into())),
2831                member: Box::new(LookupResultCallable::Callable(callable)),
2832            })
2833            .into()
2834        } else {
2835            LookupResult::from(callable).into()
2836        }
2837    } else {
2838        let mut err = |extra: &str| {
2839            let what = match &elem.borrow().base_type {
2840                ElementType::Global | ElementType::Interface => {
2841                    let enclosing_type = elem.borrow().enclosing_component.upgrade().unwrap();
2842                    assert!(enclosing_type.is_global() || enclosing_type.is_interface());
2843                    format!("'{}'", enclosing_type.id)
2844                }
2845                ElementType::Component(c) => format!("Element '{}'", c.id),
2846                ElementType::Builtin(b) => format!("Element '{}'", b.name),
2847                ElementType::Native(_) => unreachable!("the native pass comes later"),
2848                ElementType::Error => {
2849                    assert!(ctx.diag.has_errors());
2850                    return;
2851                }
2852            };
2853            ctx.diag.push_error(
2854                format!("{} does not have a property '{}'{}", what, second.text(), extra),
2855                &second,
2856            );
2857        };
2858        if let Some(minus_pos) = second.text().find('-') {
2859            // Attempt to recover if the user wanted to write "-"
2860            if elem
2861                .borrow()
2862                .lookup_property(
2863                    &crate::parser::normalize_identifier(&second.text()[0..minus_pos]),
2864                    mode,
2865                )
2866                .property_type
2867                != Type::Invalid
2868            {
2869                err(". Use space before the '-' if you meant a subtraction");
2870                return None;
2871            }
2872        }
2873        err("");
2874        None
2875    }
2876}
2877
2878fn maybe_lookup_object(
2879    mut base: LookupResult,
2880    it: impl Iterator<Item = crate::parser::SyntaxToken>,
2881    ctx: &mut LookupCtx,
2882) -> Option<LookupResult> {
2883    for next in it {
2884        let next_str = crate::parser::normalize_identifier(next.text());
2885        ctx.current_token = Some(next.clone().into());
2886        match base.lookup(ctx, &next_str) {
2887            Some(r) => {
2888                base = r;
2889            }
2890            None => {
2891                if let Some(minus_pos) = next.text().find('-')
2892                    && base.lookup(ctx, &SmolStr::new(&next.text()[0..minus_pos])).is_some()
2893                {
2894                    ctx.diag.push_error(format!("Cannot access the field '{}'. Use space before the '-' if you meant a subtraction", next.text()), &next);
2895                    return None;
2896                }
2897
2898                match base {
2899                    LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(
2900                        ..,
2901                    ))) => ctx.diag.push_error("Cannot access fields of callback".into(), &next),
2902                    LookupResult::Callable(..) => {
2903                        ctx.diag.push_error("Cannot access fields of a function".into(), &next)
2904                    }
2905                    LookupResult::Enumeration(enumeration) => ctx.diag.push_error(
2906                        format!(
2907                            "'{}' is not a member of the enum {}",
2908                            next.text(),
2909                            enumeration.name
2910                        ),
2911                        &next,
2912                    ),
2913
2914                    LookupResult::Namespace(ns) => {
2915                        ctx.diag.push_error(
2916                            format!("'{}' is not a member of the namespace {}", next.text(), ns),
2917                            &next,
2918                        );
2919                    }
2920                    LookupResult::Expression { expression, .. } => {
2921                        let ty_descr = match expression.ty() {
2922                            Type::Struct { .. } => String::new(),
2923                            Type::Float32
2924                                if ctx.property_type == Type::Model
2925                                    && matches!(
2926                                        expression,
2927                                        Expression::NumberLiteral(_, Unit::None),
2928                                    ) =>
2929                            {
2930                                // usually something like `0..foo`
2931                                format!(
2932                                    " of float. Range expressions are not supported in Slint, but you can use an integer as a model to repeat something multiple time. Eg: `for i in {}`",
2933                                    next.text()
2934                                )
2935                            }
2936
2937                            ty => format!(" of {ty}"),
2938                        };
2939                        ctx.diag.push_error(
2940                            format!("Cannot access the field '{}'{}", next.text(), ty_descr),
2941                            &next,
2942                        );
2943                    }
2944                }
2945                return None;
2946            }
2947        }
2948    }
2949    Some(base)
2950}
2951
2952/// Resolve all two way bindings on `elem`, and finalize the type of any
2953/// `property foo <=> ...` declared without an explicit type. Run after any
2954/// enclosing `for` model expression has been resolved.
2955fn resolve_two_way_bindings_for_element(
2956    elem: &ElementRc,
2957    scope: &[ElementRc],
2958    type_register: &TypeRegister,
2959    diag: &mut BuildDiagnostics,
2960) {
2961    // Queued here and applied after the loop, since the iterator holds a
2962    // borrow on `elem` that blocks `borrow_mut`.
2963    let mut to_infer: Vec<(SmolStr, Type)> = Vec::new();
2964
2965    for (prop_name, binding) in elem.borrow().real_bindings() {
2966        let mut binding = binding.borrow_mut();
2967        // The alias node is normally the binding's own (uncompiled) expression. But a
2968        // global callback may both alias another global's callback and provide a handler:
2969        // the handler then occupies the expression slot and the alias node lives on the
2970        // callback declaration, in which case the handler expression must be preserved.
2971        let twb_from_expression = match binding.value_expression() {
2972            Expression::Uncompiled(node) => syntax_nodes::TwoWayBinding::new(node.clone()),
2973            _ => None,
2974        };
2975        let twb_node = twb_from_expression
2976            .clone()
2977            .or_else(|| elem.borrow().callback_alias_declaration_node(prop_name));
2978        if let Some(n) = twb_node {
2979            let node: SyntaxNode = n.clone().into();
2980            let lhs_lookup =
2981                elem.borrow().lookup_property(prop_name, PropertyLookupMode::InternalName);
2982            if !lhs_lookup.is_valid() {
2983                // An attempt to resolve this already failed when trying to resolve the property type
2984                assert!(diag.has_errors());
2985                continue;
2986            }
2987            // Diagnostics name the property as written in the source, not by its mangled key.
2988            let declared_name = elem
2989                .borrow()
2990                .property_declarations
2991                .get(prop_name)
2992                .and_then(|d| d.shadowed_name.clone())
2993                .unwrap_or_else(|| prop_name.clone());
2994            let mut lookup_ctx = LookupCtx {
2995                property_name: Some(declared_name.as_str()),
2996                property_type: lhs_lookup.property_type.clone(),
2997                expected_type: lhs_lookup.property_type.clone(),
2998                component_scope: scope,
2999                diag,
3000                // Two-way bindings don't generate temporaries; a fresh set is fine.
3001                symbol_counters: SymbolCounters::shared(),
3002                arguments: Vec::new(),
3003                type_register,
3004                type_loader: None,
3005                current_token: Some(node.clone().into()),
3006                local_variables: Vec::new(),
3007                expected_type_probe: None,
3008            };
3009
3010            // Only the alias-only case stores the two-way binding in the expression slot;
3011            // the combined case must keep its handler expression intact.
3012            if twb_from_expression.is_some() {
3013                binding.expression = Expression::Invalid;
3014            }
3015
3016            if let Some(twb) = resolve_two_way_binding(n, &mut lookup_ctx) {
3017                if matches!(lhs_lookup.property_type, Type::InferredProperty) {
3018                    to_infer.push((prop_name.clone(), twb.ty()));
3019                }
3020                let nr = twb.property().cloned();
3021                binding.two_way_bindings.push(twb);
3022
3023                let Some(nr) = nr else { continue };
3024                nr.element()
3025                    .borrow()
3026                    .property_analysis
3027                    .borrow_mut()
3028                    .entry(nr.name().clone())
3029                    .or_default()
3030                    .is_linked = true;
3031
3032                if matches!(
3033                    lhs_lookup.property_visibility,
3034                    PropertyVisibility::Private | PropertyVisibility::Output
3035                ) && !lhs_lookup.is_local_to_component
3036                {
3037                    // invalid property assignment should have been reported earlier
3038                    assert!(diag.has_errors() || elem.borrow().is_legacy_syntax);
3039                    continue;
3040                }
3041
3042                // Check the compatibility.
3043                let mut rhs_lookup = nr
3044                    .element()
3045                    .borrow()
3046                    .lookup_property(nr.name(), PropertyLookupMode::InternalName);
3047                if rhs_lookup.property_type == Type::Invalid {
3048                    // An attempt to resolve this already failed when trying to resolve the property type
3049                    assert!(diag.has_errors());
3050                    continue;
3051                }
3052                rhs_lookup.is_local_to_component &= lookup_ctx.is_local_element(&nr.element());
3053
3054                // The derived replacement only helps callers if the target is a public property
3055                // of the same element, reached through the same object. Otherwise the hint is
3056                // unreachable, so require an explicit message instead.
3057                if elem
3058                    .borrow()
3059                    .property_declarations
3060                    .get(prop_name)
3061                    .is_some_and(|d| d.has_derived_deprecation())
3062                    && !(Rc::ptr_eq(&nr.element(), elem)
3063                        && rhs_lookup.property_visibility != PropertyVisibility::Private)
3064                {
3065                    lookup_ctx.diag.push_error(
3066                        "@deprecated without a message derives the replacement from the two-way binding target, which must be a public property of the same element; provide an explicit @deprecated(\"...\") message instead".into(),
3067                        &node,
3068                    );
3069                }
3070
3071                if !rhs_lookup.is_valid_for_assignment() {
3072                    match (lhs_lookup.property_visibility, rhs_lookup.property_visibility) {
3073                        (PropertyVisibility::Input, PropertyVisibility::Input)
3074                            if !lhs_lookup.is_local_to_component =>
3075                        {
3076                            assert!(rhs_lookup.is_local_to_component);
3077                            marked_linked_read_only(elem, prop_name);
3078                        }
3079                        (
3080                            PropertyVisibility::Output | PropertyVisibility::Private,
3081                            PropertyVisibility::Output | PropertyVisibility::Input,
3082                        ) => {
3083                            assert!(lhs_lookup.is_local_to_component);
3084                            marked_linked_read_only(elem, prop_name);
3085                        }
3086                        (PropertyVisibility::Input, PropertyVisibility::Output)
3087                            if !lhs_lookup.is_local_to_component =>
3088                        {
3089                            assert!(!rhs_lookup.is_local_to_component);
3090                            marked_linked_read_only(elem, prop_name);
3091                        }
3092                        _ => {
3093                            if lookup_ctx.is_legacy_component() {
3094                                diag.push_warning(
3095                                    format!(
3096                                        "Link to an '{}' property is deprecated",
3097                                        rhs_lookup.property_visibility
3098                                    ),
3099                                    &node,
3100                                );
3101                            } else {
3102                                diag.push_error(
3103                                    format!(
3104                                        "Cannot link to an '{}' property",
3105                                        rhs_lookup.property_visibility
3106                                    ),
3107                                    &node,
3108                                )
3109                            }
3110                        }
3111                    }
3112                } else if !lhs_lookup.is_valid_for_assignment() {
3113                    if rhs_lookup.is_local_to_component
3114                        && rhs_lookup.property_visibility == PropertyVisibility::InOut
3115                    {
3116                        if lookup_ctx.is_legacy_component() {
3117                            debug_assert!(!diag.is_empty()); // warning should already be reported
3118                        } else {
3119                            diag.push_error(
3120                                format!("Cannot link '{}' property", PropertyVisibility::Input),
3121                                &node,
3122                            );
3123                        }
3124                    } else if rhs_lookup.property_visibility == PropertyVisibility::InOut {
3125                        diag.push_warning(
3126                            format!(
3127                                "Linking '{}' properties to '{}' properties is deprecated",
3128                                PropertyVisibility::Input,
3129                                PropertyVisibility::InOut
3130                            ),
3131                            &node,
3132                        );
3133                        marked_linked_read_only(&nr.element(), nr.name());
3134                    } else {
3135                        // This is allowed, but then the rhs must also become read only.
3136                        marked_linked_read_only(&nr.element(), nr.name());
3137                    }
3138                }
3139            }
3140        }
3141    }
3142
3143    if !to_infer.is_empty() {
3144        let mut elem_mut = elem.borrow_mut();
3145        for (prop_name, inferred) in to_infer {
3146            let decl = elem_mut.property_declarations.get_mut(&prop_name).unwrap();
3147            if inferred.is_property_type() {
3148                decl.property_type = inferred;
3149            } else {
3150                let type_node = decl.type_node();
3151                diag.push_error(
3152                    format!("Could not infer type of property '{prop_name}'"),
3153                    &type_node,
3154                );
3155            }
3156        }
3157    }
3158
3159    fn marked_linked_read_only(elem: &ElementRc, prop_name: &str) {
3160        elem.borrow()
3161            .property_analysis
3162            .borrow_mut()
3163            .entry(prop_name.into())
3164            .or_default()
3165            .is_linked_to_read_only = true;
3166    }
3167}
3168
3169pub fn resolve_two_way_binding(
3170    node: syntax_nodes::TwoWayBinding,
3171    ctx: &mut LookupCtx,
3172) -> Option<TwoWayBinding> {
3173    const ERROR_MESSAGE: &str = "The expression in a two way binding must be a property reference";
3174
3175    let Some(n) = node.Expression().QualifiedName() else {
3176        ctx.diag.push_error(ERROR_MESSAGE.into(), &node.Expression());
3177        return None;
3178    };
3179
3180    let Some(r) = lookup_qualified_name_node(n, ctx, LookupPhase::ResolvingTwoWayBindings) else {
3181        assert!(ctx.diag.has_errors());
3182        return None;
3183    };
3184
3185    // If type is invalid, error has already been reported,  when inferring, the error will be reported by the inferring code
3186    let report_error = !matches!(
3187        ctx.property_type,
3188        Type::InferredProperty | Type::InferredCallback | Type::Invalid
3189    );
3190    match r {
3191        LookupResult::Expression { expression, .. } => {
3192            fn unwrap_fields(expression: &Expression) -> Option<TwoWayBinding> {
3193                match expression {
3194                    Expression::PropertyReference(nr) => Some(nr.clone().into()),
3195                    Expression::StructFieldAccess { base, name } => {
3196                        let mut prop = unwrap_fields(base)?;
3197                        let field_access = match &mut prop {
3198                            TwoWayBinding::Property { field_access, .. } => field_access,
3199                            TwoWayBinding::ModelData { field_access, .. } => field_access,
3200                        };
3201                        field_access.push(name.clone());
3202                        Some(prop)
3203                    }
3204                    Expression::RepeaterModelReference { element } => {
3205                        Some(TwoWayBinding::ModelData {
3206                            repeated_element: element.clone(),
3207                            field_access: vec![],
3208                        })
3209                    }
3210                    _ => None,
3211                }
3212            }
3213            if let Some(result) = unwrap_fields(&expression) {
3214                // Walk the `ModelData` field path now: the qualified-name
3215                // lookup built it without type checks (the row type may not
3216                // have been known yet). Emits per-field diagnostics and
3217                // yields the leaf type as `expr_ty`.
3218                let expr_ty = if let TwoWayBinding::ModelData { repeated_element, field_access } =
3219                    &result
3220                {
3221                    let mut ty =
3222                        Expression::RepeaterModelReference { element: repeated_element.clone() }
3223                            .ty();
3224                    if !matches!(ty, Type::Invalid) {
3225                        for f in field_access {
3226                            let next = if let Type::Struct(s) = &ty {
3227                                s.fields.get(f.as_str()).cloned()
3228                            } else {
3229                                None
3230                            };
3231                            let Some(next) = next else {
3232                                ctx.diag.push_error(
3233                                    format!("Cannot access the field '{f}' of {ty}"),
3234                                    &node,
3235                                );
3236                                return None;
3237                            };
3238                            ty = next;
3239                        }
3240                    }
3241                    ty
3242                } else {
3243                    result.ty()
3244                };
3245                if report_error && expr_ty != ctx.property_type {
3246                    ctx.diag.push_error(
3247                        format!(
3248                            "The property '{}' does not have the same type as the bound expression: {} != {expr_ty}",
3249                            ctx.property_name.unwrap_or(""),
3250                            ctx.property_type,
3251                        ),
3252                        &node,
3253                    );
3254                }
3255                Some(result)
3256            } else {
3257                let kind = match expression {
3258                    Expression::StructFieldAccess { .. } | Expression::ArrayIndex { .. } => {
3259                        "Two-way bindings can only target property references"
3260                    }
3261                    _ => ERROR_MESSAGE,
3262                };
3263                ctx.diag.push_error(kind.into(), &node);
3264                None
3265            }
3266        }
3267        LookupResult::Callable(LookupResultCallable::Callable(Callable::Callback(n))) => {
3268            if report_error && n.ty() != ctx.property_type {
3269                ctx.diag.push_error("Cannot bind to a callback".into(), &node);
3270                None
3271            } else {
3272                Some(n.into())
3273            }
3274        }
3275        LookupResult::Callable(..) => {
3276            if report_error {
3277                ctx.diag.push_error("Cannot bind to a function".into(), &node);
3278            }
3279            None
3280        }
3281        _ => {
3282            ctx.diag.push_error(ERROR_MESSAGE.into(), &node);
3283            None
3284        }
3285    }
3286}
3287
3288/// For connection to callback aliases, some check are to be performed later
3289fn check_callback_alias_validity(
3290    node: &syntax_nodes::CallbackConnection,
3291    elem: &ElementRc,
3292    name: &str,
3293    diag: &mut BuildDiagnostics,
3294) {
3295    let elem_borrow = elem.borrow();
3296    let Some(decl) = elem_borrow.property_declarations.get(name) else {
3297        if let ElementType::Component(c) = &elem_borrow.base_type {
3298            check_callback_alias_validity(node, &c.root_element, name, diag);
3299        }
3300        return;
3301    };
3302    let Some(b) = elem_borrow.binding_cell_including_synthetic(name) else { return };
3303    // `try_borrow` because we might be called for the current binding
3304    let Some(alias) = b
3305        .try_borrow()
3306        .ok()
3307        .and_then(|b| b.two_way_bindings.first().and_then(|x| x.property()).cloned())
3308    else {
3309        return;
3310    };
3311
3312    // A non-global element can be instantiated many times, so letting it assign a handler
3313    // to a singleton global's callback is ambiguous. A global is itself a singleton, so it
3314    // may implement another global's callback.
3315    if alias.element().borrow().base_type == ElementType::Global
3316        && elem_borrow.base_type != ElementType::Global
3317    {
3318        diag.push_error(
3319            "Can't assign a local callback handler to an alias to a global callback".into(),
3320            &node.child_token(SyntaxKind::Identifier).unwrap(),
3321        );
3322    }
3323    if let Type::Callback(callback) = &decl.property_type {
3324        let num_arg = node.DeclaredIdentifier().count();
3325        if num_arg > callback.args.len() {
3326            diag.push_error(
3327                format!(
3328                    "'{name}' only has {} arguments, but {num_arg} were provided",
3329                    callback.args.len(),
3330                ),
3331                &node.child_token(SyntaxKind::Identifier).unwrap(),
3332            );
3333        }
3334    }
3335}
3336
3337/// Validate a callback handler body against the Slint SC subset: a sequence of
3338/// callback invocations, and nothing else.
3339///
3340/// The expressions a handler body may be made of are each rejected where they
3341/// are resolved; what's left to reject here is an expression that's in the
3342/// subset on its own but has no effect as a statement, such as a property read.
3343#[cfg(feature = "slint-sc")]
3344fn check_slint_sc_handler_body(
3345    expr: &Expression,
3346    node: &syntax_nodes::CallbackConnection,
3347    ctx: &mut LookupCtx,
3348) {
3349    let statements = match expr {
3350        Expression::CodeBlock(statements) => statements.as_slice(),
3351        single => core::slice::from_ref(single),
3352    };
3353    if !statements.iter().all(|statement| {
3354        matches!(
3355            statement,
3356            // An error was already reported for this statement.
3357            Expression::Invalid | Expression::FunctionCall { function: Callable::Callback(..), .. }
3358        )
3359    }) {
3360        // Report on the name of the callback: the handler itself spans as many
3361        // lines as its body.
3362        let name = node.child_token(SyntaxKind::Identifier);
3363        ctx.diag.slint_sc_error(
3364            "A callback handler body that isn't a callback invocation is",
3365            name.as_ref().map_or(&**node as &dyn Spanned, |name| name),
3366        );
3367    }
3368}