i_slint_compiler/parser/
expressions.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::document::parse_qualified_name;
5use super::prelude::*;
6
7#[cfg_attr(test, parser_test)]
8/// ```test,Expression
9/// something
10/// "something"
11/// 0.3
12/// 42
13/// 42px
14/// #aabbcc
15/// (something)
16/// (something).something
17/// @image-url("something")
18/// @image_url("something")
19/// some_id.some_property
20/// function_call()
21/// function_call(hello, world)
22/// cond ? first : second
23/// call_cond() ? first : second
24/// (nested()) ? (ok) : (other.ko)
25/// 4 + 4
26/// 4 + 8 * 7 / 5 + 3 - 7 - 7 * 8
27/// -0.3px + 0.3px - 3.pt+3pt
28/// aa == cc && bb && (xxx || fff) && 3 + aaa == bbb
29/// [array]
30/// array[index]
31/// {object:42}
32/// "foo".bar.something().something.xx({a: 1.foo}.a)
33/// ```
34pub fn parse_expression(p: &mut impl Parser) -> bool {
35    p.peek(); // consume the whitespace so they aren't part of the Expression node
36    parse_expression_helper(p, OperatorPrecedence::Default)
37}
38
39#[derive(Eq, PartialEq, Ord, PartialOrd)]
40#[repr(u8)]
41enum OperatorPrecedence {
42    /// ` ?: `
43    Default,
44    /// `||`, `&&`
45    Logical,
46    /// `==` `!=` `>=` `<=` `<` `>`
47    Equality,
48    /// `+ -`
49    Add,
50    /// `* /`
51    Mul,
52    Unary,
53}
54
55fn parse_expression_helper(p: &mut impl Parser, precedence: OperatorPrecedence) -> bool {
56    let mut p = p.start_node(SyntaxKind::Expression);
57    let checkpoint = p.checkpoint();
58    let mut possible_range = false;
59    match p.nth(0).kind() {
60        SyntaxKind::Identifier => {
61            parse_qualified_name(&mut *p);
62        }
63        SyntaxKind::StringLiteral => {
64            if p.nth(0).as_str().ends_with('{') {
65                parse_template_string(&mut *p)
66            } else {
67                p.consume()
68            }
69        }
70        SyntaxKind::NumberLiteral => {
71            if p.nth(0).as_str().ends_with('.') {
72                possible_range = true;
73            }
74            p.consume()
75        }
76        SyntaxKind::ColorLiteral => p.consume(),
77        SyntaxKind::LParent => {
78            p.consume();
79            parse_expression(&mut *p);
80            p.expect(SyntaxKind::RParent);
81        }
82        SyntaxKind::LBracket => parse_array(&mut *p),
83        SyntaxKind::LBrace => parse_object_notation(&mut *p),
84        SyntaxKind::Plus | SyntaxKind::Minus | SyntaxKind::Bang => {
85            let mut p = p.start_node(SyntaxKind::UnaryOpExpression);
86            p.consume();
87            parse_expression_helper(&mut *p, OperatorPrecedence::Unary);
88        }
89        SyntaxKind::At => {
90            parse_at_keyword(&mut *p);
91        }
92        _ => {
93            p.error("invalid expression");
94            return false;
95        }
96    }
97
98    loop {
99        match p.nth(0).kind() {
100            SyntaxKind::Dot => {
101                {
102                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
103                }
104                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::MemberAccess);
105                p.consume(); // '.'
106                if possible_range && p.peek().kind() == SyntaxKind::NumberLiteral {
107                    let error = format!("Parse error. Range expressions are not supported in Slint. You can use an integer as a model to repeat something multiple time. Eg: `for i in {} : ...`", p.peek().as_str());
108                    p.error(error);
109                    p.consume();
110                    return false;
111                }
112                if !p.expect(SyntaxKind::Identifier) {
113                    return false;
114                }
115            }
116            SyntaxKind::LParent => {
117                {
118                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
119                }
120                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::FunctionCallExpression);
121                parse_function_arguments(&mut *p);
122            }
123            SyntaxKind::LBracket => {
124                {
125                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
126                }
127                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::IndexExpression);
128                p.expect(SyntaxKind::LBracket);
129                parse_expression(&mut *p);
130                p.expect(SyntaxKind::RBracket);
131            }
132            _ => break,
133        }
134        possible_range = false;
135    }
136
137    if precedence >= OperatorPrecedence::Mul {
138        return true;
139    }
140
141    while matches!(p.nth(0).kind(), SyntaxKind::Star | SyntaxKind::Div) {
142        {
143            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
144        }
145        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
146        p.consume();
147        parse_expression_helper(&mut *p, OperatorPrecedence::Mul);
148    }
149
150    if p.nth(0).kind() == SyntaxKind::Percent {
151        p.error("Unexpected '%'. For the unit, it should be attached to the number. If you're looking for the modulo operator, use the 'Math.mod(x, y)' function");
152        p.consume();
153        return false;
154    }
155
156    if precedence >= OperatorPrecedence::Add {
157        return true;
158    }
159
160    while matches!(p.nth(0).kind(), SyntaxKind::Plus | SyntaxKind::Minus) {
161        {
162            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
163        }
164        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
165        p.consume();
166        parse_expression_helper(&mut *p, OperatorPrecedence::Add);
167    }
168
169    if precedence > OperatorPrecedence::Equality {
170        return true;
171    }
172
173    if matches!(
174        p.nth(0).kind(),
175        SyntaxKind::LessEqual
176            | SyntaxKind::GreaterEqual
177            | SyntaxKind::EqualEqual
178            | SyntaxKind::NotEqual
179            | SyntaxKind::LAngle
180            | SyntaxKind::RAngle
181    ) {
182        if precedence == OperatorPrecedence::Equality {
183            p.error("Use parentheses to disambiguate equality expression on the same level");
184        }
185
186        {
187            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
188        }
189        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
190        p.consume();
191        parse_expression_helper(&mut *p, OperatorPrecedence::Equality);
192    }
193
194    if precedence >= OperatorPrecedence::Logical {
195        return true;
196    }
197
198    let mut prev_logical_op = None;
199    while matches!(p.nth(0).kind(), SyntaxKind::AndAnd | SyntaxKind::OrOr) {
200        if let Some(prev) = prev_logical_op {
201            if prev != p.nth(0).kind() {
202                p.error("Use parentheses to disambiguate between && and ||");
203                prev_logical_op = None;
204            }
205        } else {
206            prev_logical_op = Some(p.nth(0).kind());
207        }
208
209        {
210            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
211        }
212        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
213        p.consume();
214        parse_expression_helper(&mut *p, OperatorPrecedence::Logical);
215    }
216
217    if p.nth(0).kind() == SyntaxKind::Question {
218        {
219            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
220        }
221        let mut p = p.start_node_at(checkpoint, SyntaxKind::ConditionalExpression);
222        p.consume();
223        parse_expression(&mut *p);
224        p.expect(SyntaxKind::Colon);
225        parse_expression(&mut *p);
226    }
227    true
228}
229
230#[cfg_attr(test, parser_test)]
231/// ```test
232/// @image-url("/foo/bar.png")
233/// @linear-gradient(0deg, blue, red)
234/// @conic-gradient(blue 0deg, red 180deg)
235/// @tr("foo", bar)
236/// ```
237fn parse_at_keyword(p: &mut impl Parser) {
238    debug_assert_eq!(p.peek().kind(), SyntaxKind::At);
239    match p.nth(1).as_str() {
240        "image-url" | "image_url" => {
241            parse_image_url(p);
242        }
243        "linear-gradient" | "linear_gradient" => {
244            parse_gradient(p);
245        }
246        "radial-gradient" | "radial_gradient" => {
247            parse_gradient(p);
248        }
249        "conic-gradient" | "conic_gradient" => {
250            parse_gradient(p);
251        }
252        "tr" => {
253            parse_tr(p);
254        }
255        _ => {
256            p.consume();
257            p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works
258            p.error("Expected 'image-url', 'tr', 'linear-gradient', 'radial-gradient' or 'conic-gradient' after '@'");
259        }
260    }
261}
262
263#[cfg_attr(test, parser_test)]
264/// ```test,Array
265/// [ a, b, c , d]
266/// []
267/// [a,]
268/// [ [], [] ]
269/// ```
270fn parse_array(p: &mut impl Parser) {
271    let mut p = p.start_node(SyntaxKind::Array);
272    p.expect(SyntaxKind::LBracket);
273
274    while p.nth(0).kind() != SyntaxKind::RBracket {
275        parse_expression(&mut *p);
276        if !p.test(SyntaxKind::Comma) {
277            break;
278        }
279    }
280    p.expect(SyntaxKind::RBracket);
281}
282
283#[cfg_attr(test, parser_test)]
284/// ```test,ObjectLiteral
285/// {}
286/// {a:b}
287/// { a: "foo" , }
288/// {a:b, c: 4 + 4, d: [a,] }
289/// ```
290fn parse_object_notation(p: &mut impl Parser) {
291    let mut p = p.start_node(SyntaxKind::ObjectLiteral);
292    p.expect(SyntaxKind::LBrace);
293
294    while p.nth(0).kind() != SyntaxKind::RBrace {
295        let mut p = p.start_node(SyntaxKind::ObjectMember);
296        p.expect(SyntaxKind::Identifier);
297        p.expect(SyntaxKind::Colon);
298        parse_expression(&mut *p);
299        if !p.test(SyntaxKind::Comma) {
300            break;
301        }
302    }
303    p.expect(SyntaxKind::RBrace);
304}
305
306#[cfg_attr(test, parser_test)]
307/// ```test
308/// ()
309/// (foo)
310/// (foo, bar, foo)
311/// (foo, bar(), xx+xx,)
312/// ```
313fn parse_function_arguments(p: &mut impl Parser) {
314    p.expect(SyntaxKind::LParent);
315
316    while p.nth(0).kind() != SyntaxKind::RParent {
317        parse_expression(&mut *p);
318        if !p.test(SyntaxKind::Comma) {
319            break;
320        }
321    }
322    p.expect(SyntaxKind::RParent);
323}
324
325#[cfg_attr(test, parser_test)]
326/// ```test,StringTemplate
327/// "foo\{bar}"
328/// "foo\{4 + 5}foo"
329/// ```
330fn parse_template_string(p: &mut impl Parser) {
331    let mut p = p.start_node(SyntaxKind::StringTemplate);
332    debug_assert!(p.nth(0).as_str().ends_with("\\{"));
333    {
334        let mut p = p.start_node(SyntaxKind::Expression);
335        p.expect(SyntaxKind::StringLiteral);
336    }
337    loop {
338        parse_expression(&mut *p);
339        let peek = p.peek();
340        if peek.kind != SyntaxKind::StringLiteral || !peek.as_str().starts_with('}') {
341            p.error("Error while parsing string template")
342        }
343        let mut p = p.start_node(SyntaxKind::Expression);
344        let cont = peek.as_str().ends_with('{');
345        p.consume();
346        if !cont {
347            break;
348        }
349    }
350}
351
352#[cfg_attr(test, parser_test)]
353/// ```test,AtGradient
354/// @linear-gradient(#e66465, #9198e5)
355/// @linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c)
356/// @linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%)
357/// @linear-gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
358/// @linear_gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
359/// @radial-gradient(circle, #e66465, blue 50%, #9198e5)
360/// @conic-gradient(#e66465 0deg, #9198e5 180deg, #e66465 360deg)
361/// @conic-gradient(red 0deg, green 120deg, blue 240deg, red 360deg)
362/// @conic-gradient(#fff 0turn, #000 0.5turn, #fff 1turn)
363/// @conic_gradient(red 0rad, blue 3.14159rad, red 6.28318rad)
364/// ```
365fn parse_gradient(p: &mut impl Parser) {
366    let mut p = p.start_node(SyntaxKind::AtGradient);
367    p.expect(SyntaxKind::At);
368    debug_assert!(p.peek().as_str().ends_with("gradient"));
369    p.expect(SyntaxKind::Identifier); //eg "linear-gradient"
370
371    p.expect(SyntaxKind::LParent);
372
373    while !p.test(SyntaxKind::RParent) {
374        if !parse_expression(&mut *p) {
375            return;
376        }
377        p.test(SyntaxKind::Comma);
378    }
379}
380
381#[cfg_attr(test, parser_test)]
382/// ```test,AtTr
383/// @tr("foo")
384/// @tr("foo{0}", bar(42))
385/// @tr("context" => "ccc{}", 0)
386/// @tr("xxx" => "ccc{n}" | "ddd{}" % 42, 45)
387/// ```
388fn parse_tr(p: &mut impl Parser) {
389    let mut p = p.start_node(SyntaxKind::AtTr);
390    p.expect(SyntaxKind::At);
391    debug_assert_eq!(p.peek().as_str(), "tr");
392    p.expect(SyntaxKind::Identifier); //"tr"
393    p.expect(SyntaxKind::LParent);
394
395    let checkpoint = p.checkpoint();
396
397    fn consume_literal(p: &mut impl Parser) -> bool {
398        let peek = p.peek();
399        if peek.kind() != SyntaxKind::StringLiteral
400            || !peek.as_str().starts_with('"')
401            || !peek.as_str().ends_with('"')
402        {
403            p.error("Expected plain string literal");
404            return false;
405        }
406        p.expect(SyntaxKind::StringLiteral)
407    }
408
409    if !consume_literal(&mut *p) {
410        return;
411    }
412
413    if p.test(SyntaxKind::FatArrow) {
414        drop(p.start_node_at(checkpoint, SyntaxKind::TrContext));
415        if !consume_literal(&mut *p) {
416            return;
417        }
418    }
419
420    if p.peek().kind() == SyntaxKind::Pipe {
421        let mut p = p.start_node(SyntaxKind::TrPlural);
422        p.consume();
423        if !consume_literal(&mut *p) || !p.expect(SyntaxKind::Percent) {
424            let _ = p.start_node(SyntaxKind::Expression);
425            return;
426        }
427        parse_expression(&mut *p);
428    }
429
430    while p.test(SyntaxKind::Comma) {
431        if !parse_expression(&mut *p) {
432            break;
433        }
434    }
435    p.expect(SyntaxKind::RParent);
436}
437
438#[cfg_attr(test, parser_test)]
439/// ```test,AtImageUrl
440/// @image-url("foo.png")
441/// @image-url("foo.png",)
442/// @image-url("foo.png", nine-slice(1 2 3 4))
443/// @image-url("foo.png", nine-slice(1))
444/// ```
445fn parse_image_url(p: &mut impl Parser) {
446    let mut p = p.start_node(SyntaxKind::AtImageUrl);
447    p.consume(); // "@"
448    p.consume(); // "image-url"
449    if !(p.expect(SyntaxKind::LParent)) {
450        return;
451    }
452    let peek = p.peek();
453    if peek.kind() != SyntaxKind::StringLiteral {
454        p.error("@image-url must contain a plain path as a string literal");
455        p.until(SyntaxKind::RParent);
456        return;
457    }
458    if !peek.as_str().starts_with('"') || !peek.as_str().ends_with('"') {
459        p.error("@image-url must contain a plain path as a string literal, without any '\\{}' expressions");
460        p.until(SyntaxKind::RParent);
461        return;
462    }
463    p.expect(SyntaxKind::StringLiteral);
464    if !p.test(SyntaxKind::Comma) {
465        if !p.test(SyntaxKind::RParent) {
466            p.error("Expected ')' or ','");
467            p.until(SyntaxKind::RParent);
468        }
469        return;
470    }
471    if p.test(SyntaxKind::RParent) {
472        return;
473    }
474    if p.peek().as_str() != "nine-slice" {
475        p.error("Expected 'nine-slice(...)' argument");
476        p.until(SyntaxKind::RParent);
477        return;
478    }
479    p.consume();
480    if !p.expect(SyntaxKind::LParent) {
481        p.until(SyntaxKind::RParent);
482        return;
483    }
484    let mut count = 0;
485    loop {
486        match p.peek().kind() {
487            SyntaxKind::RParent => {
488                if count != 1 && count != 2 && count != 4 {
489                    p.error("Expected 1 or 2 or 4 numbers");
490                }
491                p.consume();
492                break;
493            }
494            SyntaxKind::NumberLiteral => {
495                count += 1;
496                p.consume();
497            }
498            SyntaxKind::Comma | SyntaxKind::Colon => {
499                p.error("Arguments of nine-slice need to be separated by spaces");
500                p.until(SyntaxKind::RParent);
501                break;
502            }
503            _ => {
504                p.error("Expected number literal or ')'");
505                p.until(SyntaxKind::RParent);
506                break;
507            }
508        }
509    }
510    if !p.expect(SyntaxKind::RParent) {
511        p.until(SyntaxKind::RParent);
512    }
513}