Skip to main content

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!(
108                        "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 {} : ...`",
109                        p.peek().as_str()
110                    );
111                    p.error(error);
112                    p.consume();
113                    return false;
114                }
115                if !p.expect(SyntaxKind::Identifier) {
116                    return false;
117                }
118            }
119            SyntaxKind::LParent => {
120                {
121                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
122                }
123                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::FunctionCallExpression);
124                parse_function_arguments(&mut *p);
125            }
126            SyntaxKind::LBracket => {
127                {
128                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
129                }
130                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::IndexExpression);
131                p.expect(SyntaxKind::LBracket);
132                parse_expression(&mut *p);
133                p.expect(SyntaxKind::RBracket);
134            }
135            _ => break,
136        }
137        possible_range = false;
138    }
139
140    if precedence >= OperatorPrecedence::Mul {
141        return true;
142    }
143
144    while matches!(p.nth(0).kind(), SyntaxKind::Star | SyntaxKind::Div) {
145        {
146            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
147        }
148        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
149        p.consume();
150        parse_expression_helper(&mut *p, OperatorPrecedence::Mul);
151    }
152
153    if p.nth(0).kind() == SyntaxKind::Percent {
154        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");
155        p.consume();
156        return false;
157    }
158
159    if precedence >= OperatorPrecedence::Add {
160        return true;
161    }
162
163    while matches!(p.nth(0).kind(), SyntaxKind::Plus | SyntaxKind::Minus) {
164        {
165            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
166        }
167        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
168        p.consume();
169        parse_expression_helper(&mut *p, OperatorPrecedence::Add);
170    }
171
172    if precedence > OperatorPrecedence::Equality {
173        return true;
174    }
175
176    if matches!(
177        p.nth(0).kind(),
178        SyntaxKind::LessEqual
179            | SyntaxKind::GreaterEqual
180            | SyntaxKind::EqualEqual
181            | SyntaxKind::NotEqual
182            | SyntaxKind::LAngle
183            | SyntaxKind::RAngle
184    ) {
185        if precedence == OperatorPrecedence::Equality {
186            p.error("Use parentheses to disambiguate equality expression on the same level");
187        }
188
189        {
190            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
191        }
192        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
193        p.consume();
194        parse_expression_helper(&mut *p, OperatorPrecedence::Equality);
195    }
196
197    if precedence >= OperatorPrecedence::Logical {
198        return true;
199    }
200
201    let mut prev_logical_op = None;
202    while matches!(p.nth(0).kind(), SyntaxKind::AndAnd | SyntaxKind::OrOr) {
203        if let Some(prev) = prev_logical_op {
204            if prev != p.nth(0).kind() {
205                p.error("Use parentheses to disambiguate between && and ||");
206                prev_logical_op = None;
207            }
208        } else {
209            prev_logical_op = Some(p.nth(0).kind());
210        }
211
212        {
213            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
214        }
215        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
216        p.consume();
217        parse_expression_helper(&mut *p, OperatorPrecedence::Logical);
218    }
219
220    if p.nth(0).kind() == SyntaxKind::Question {
221        {
222            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
223        }
224        let mut p = p.start_node_at(checkpoint, SyntaxKind::ConditionalExpression);
225        p.consume();
226        parse_expression(&mut *p);
227        p.expect(SyntaxKind::Colon);
228        parse_expression(&mut *p);
229    }
230    true
231}
232
233#[cfg_attr(test, parser_test)]
234/// ```test
235/// @image-url("/foo/bar.png")
236/// @linear-gradient(0deg, blue, red)
237/// @conic-gradient(blue 0deg, red 180deg)
238/// @tr("foo", bar)
239/// ```
240fn parse_at_keyword(p: &mut impl Parser) {
241    debug_assert_eq!(p.peek().kind(), SyntaxKind::At);
242    match p.nth(1).as_str() {
243        "image-url" | "image_url" => {
244            parse_image_url(p);
245        }
246        "linear-gradient" | "linear_gradient" => {
247            parse_gradient(p);
248        }
249        "radial-gradient" | "radial_gradient" => {
250            parse_gradient(p);
251        }
252        "conic-gradient" | "conic_gradient" => {
253            parse_gradient(p);
254        }
255        "tr" => {
256            parse_tr(p);
257        }
258        "markdown" => {
259            parse_markdown(p);
260        }
261        "keys" => {
262            parse_keys(p);
263        }
264        _ => {
265            p.consume();
266            p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works
267            p.error("Expected 'image-url', 'tr', 'keys', 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'");
268        }
269    }
270}
271
272#[cfg_attr(test, parser_test)]
273/// ```test,Array
274/// [ a, b, c , d]
275/// []
276/// [a,]
277/// [ [], [] ]
278/// ```
279fn parse_array(p: &mut impl Parser) {
280    let mut p = p.start_node(SyntaxKind::Array);
281    p.expect(SyntaxKind::LBracket);
282
283    while p.nth(0).kind() != SyntaxKind::RBracket {
284        parse_expression(&mut *p);
285        if !p.test(SyntaxKind::Comma) {
286            break;
287        }
288    }
289    p.expect(SyntaxKind::RBracket);
290}
291
292#[cfg_attr(test, parser_test)]
293/// ```test,ObjectLiteral
294/// {}
295/// {a:b}
296/// { a: "foo" , }
297/// {a:b, c: 4 + 4, d: [a,] }
298/// ```
299fn parse_object_notation(p: &mut impl Parser) {
300    let mut p = p.start_node(SyntaxKind::ObjectLiteral);
301    p.expect(SyntaxKind::LBrace);
302
303    while p.nth(0).kind() != SyntaxKind::RBrace {
304        let mut p = p.start_node(SyntaxKind::ObjectMember);
305        p.expect(SyntaxKind::Identifier);
306        p.expect(SyntaxKind::Colon);
307        parse_expression(&mut *p);
308        if !p.test(SyntaxKind::Comma) {
309            break;
310        }
311    }
312    p.expect(SyntaxKind::RBrace);
313}
314
315#[cfg_attr(test, parser_test)]
316/// ```test
317/// ()
318/// (foo)
319/// (foo, bar, foo)
320/// (foo, bar(), xx+xx,)
321/// ```
322fn parse_function_arguments(p: &mut impl Parser) {
323    p.expect(SyntaxKind::LParent);
324
325    while p.nth(0).kind() != SyntaxKind::RParent {
326        parse_expression(&mut *p);
327        if !p.test(SyntaxKind::Comma) {
328            break;
329        }
330    }
331    p.expect(SyntaxKind::RParent);
332}
333
334#[cfg_attr(test, parser_test)]
335/// ```test,StringTemplate
336/// "foo\{bar}"
337/// "foo\{4 + 5}foo"
338/// ```
339fn parse_template_string(p: &mut impl Parser) {
340    let mut p = p.start_node(SyntaxKind::StringTemplate);
341    debug_assert!(p.nth(0).as_str().ends_with("\\{"));
342    p.expect(SyntaxKind::StringLiteral);
343    loop {
344        parse_expression(&mut *p);
345        let peek = p.peek();
346        if peek.kind != SyntaxKind::StringLiteral || !peek.as_str().starts_with('}') {
347            p.error("Error while parsing string template")
348        }
349        let cont = peek.as_str().ends_with('{');
350        p.consume();
351        if !cont {
352            break;
353        }
354    }
355}
356
357#[cfg_attr(test, parser_test)]
358/// ```test,AtGradient
359/// @linear-gradient(#e66465, #9198e5)
360/// @linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c)
361/// @linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%)
362/// @linear-gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
363/// @linear_gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
364/// @radial-gradient(circle, #e66465, blue 50%, #9198e5)
365/// @conic-gradient(#e66465 0deg, #9198e5 180deg, #e66465 360deg)
366/// @conic-gradient(red 0deg, green 120deg, blue 240deg, red 360deg)
367/// @conic-gradient(#fff 0turn, #000 0.5turn, #fff 1turn)
368/// @conic_gradient(red 0rad, blue 3.14159rad, red 6.28318rad)
369/// ```
370fn parse_gradient(p: &mut impl Parser) {
371    let mut p = p.start_node(SyntaxKind::AtGradient);
372    p.expect(SyntaxKind::At);
373    debug_assert!(p.peek().as_str().ends_with("gradient"));
374    p.expect(SyntaxKind::Identifier); //eg "linear-gradient"
375
376    p.expect(SyntaxKind::LParent);
377
378    while !p.test(SyntaxKind::RParent) {
379        if !parse_expression(&mut *p) {
380            return;
381        }
382        p.test(SyntaxKind::Comma);
383    }
384}
385
386#[cfg_attr(test, parser_test)]
387/// ```test,AtTr
388/// @tr("foo")
389/// @tr("foo{0}", bar(42))
390/// @tr("context" => "ccc{}", 0)
391/// @tr("xxx" => "ccc{n}" | "ddd{}" % 42, 45)
392/// ```
393fn parse_tr(p: &mut impl Parser) {
394    let mut p = p.start_node(SyntaxKind::AtTr);
395    p.expect(SyntaxKind::At);
396    debug_assert_eq!(p.peek().as_str(), "tr");
397    p.expect(SyntaxKind::Identifier); //"tr"
398    p.expect(SyntaxKind::LParent);
399
400    let checkpoint = p.checkpoint();
401
402    fn consume_literal(p: &mut impl Parser) -> bool {
403        let peek = p.peek();
404        if peek.kind() != SyntaxKind::StringLiteral
405            || !peek.as_str().starts_with('"')
406            || !peek.as_str().ends_with('"')
407        {
408            p.error("Expected plain string literal");
409            return false;
410        }
411        p.expect(SyntaxKind::StringLiteral)
412    }
413
414    if !consume_literal(&mut *p) {
415        return;
416    }
417
418    if p.test(SyntaxKind::FatArrow) {
419        drop(p.start_node_at(checkpoint, SyntaxKind::TrContext));
420        if !consume_literal(&mut *p) {
421            return;
422        }
423    }
424
425    if p.peek().kind() == SyntaxKind::Pipe {
426        let mut p = p.start_node(SyntaxKind::TrPlural);
427        p.consume();
428        if !consume_literal(&mut *p) || !p.expect(SyntaxKind::Percent) {
429            let _ = p.start_node(SyntaxKind::Expression);
430            return;
431        }
432        parse_expression(&mut *p);
433    }
434
435    while p.test(SyntaxKind::Comma) {
436        if !parse_expression(&mut *p) {
437            break;
438        }
439    }
440    p.expect(SyntaxKind::RParent);
441}
442
443/// ```test,AtTr
444/// @markdown("foo")
445/// @markdown("foo\{bar(42)} xx")
446/// @markdown("foo\n" "bar")
447/// ```
448fn parse_markdown(p: &mut impl Parser) {
449    let mut p = p.start_node(SyntaxKind::AtMarkdown);
450    p.expect(SyntaxKind::At);
451    debug_assert!(p.peek().as_str().ends_with("markdown"));
452    p.expect(SyntaxKind::Identifier); //eg "markdown"
453    p.expect(SyntaxKind::LParent);
454
455    let mut has_content = false;
456    loop {
457        let peek = p.peek();
458        if peek.kind() != SyntaxKind::StringLiteral {
459            break;
460        }
461        if peek.as_str().ends_with('{') {
462            parse_template_string(&mut *p)
463        } else {
464            p.consume()
465        }
466        has_content = true;
467    }
468
469    if !has_content {
470        p.error("Expected string literal");
471        p.until(SyntaxKind::RParent);
472        return;
473    }
474
475    if !p.expect(SyntaxKind::RParent) {
476        p.until(SyntaxKind::RParent);
477    }
478}
479
480#[cfg_attr(test, parser_test)]
481/// ```test,AtKeys
482/// @keys()
483/// @keys("x")
484/// @keys(Control +Shift + Alt+Meta+"A")
485/// @keys(Control +Shift + Alt+Meta+Return)
486/// @keys(Control +Shift? + Alt+Meta+Return)
487/// @keys(Control +Shift + Alt?+Meta+Return)
488/// ```
489fn parse_keys(p: &mut impl Parser) {
490    let mut p = p.start_node(SyntaxKind::AtKeys);
491    p.expect(SyntaxKind::At);
492    debug_assert_eq!(p.peek().as_str(), "keys");
493    p.expect(SyntaxKind::Identifier); //"keys"
494    p.expect(SyntaxKind::LParent);
495
496    // Parse custom syntax here...
497    let mut key_count = 0_u32;
498
499    let mut alt_count = 0_u32;
500    let mut control_count = 0_u32;
501    let mut shift_count = 0_u32;
502    let mut meta_count = 0_u32;
503    let mut ignore_shift_count = 0_u32;
504    let mut ignore_alt_count = 0_u32;
505
506    #[derive(Eq, PartialEq)]
507    enum State {
508        Start,
509        NeedPlus,
510        NeedKey,
511    }
512    let mut state = State::Start;
513
514    fn bail(p: &mut crate::parser::Node<'_, impl Parser>, message: &str) {
515        p.error(message);
516        p.until(SyntaxKind::RParent);
517    }
518
519    loop {
520        match p.peek().kind() {
521            SyntaxKind::RParent => {
522                assert!(key_count <= 1);
523                // Trailing plus
524                if state == State::NeedKey {
525                    p.error("Expected another identifier or string literal");
526                } else if key_count == 0
527                    && (alt_count + control_count + shift_count + meta_count) > 0
528                {
529                    p.error("A keyboard shortcut must be empty or contain exactly one key (with modifiers)");
530                }
531                p.consume();
532                break;
533            }
534            SyntaxKind::Plus => {
535                if state == State::NeedPlus {
536                    state = State::NeedKey;
537                    p.consume();
538                } else {
539                    bail(
540                        &mut p,
541                        "Unexpected '+' in keyboard shortcut (use Plus to refer to the key)",
542                    );
543                    break;
544                }
545                continue;
546            }
547            SyntaxKind::Identifier | SyntaxKind::StringLiteral => {
548                if state == State::NeedPlus {
549                    bail(&mut p, "Expected '+' to separate parts of a keyboard shortcut");
550                    break;
551                }
552
553                let token = p.peek();
554                let mut consume_count = 1;
555                // Modifiers must be identifiers, not string literals
556                if token.kind() == SyntaxKind::Identifier {
557                    let text = token.as_str();
558
559                    let mut try_consume_question = || -> bool {
560                        let next_token = p.nth(1);
561                        if next_token.kind() == SyntaxKind::Question {
562                            consume_count += 1;
563                            true
564                        } else {
565                            false
566                        }
567                    };
568
569                    match text {
570                        "Ctrl" => {
571                            bail(&mut p, "Ctrl is not in the Key namespace (Use Control instead)");
572                            break;
573                        }
574                        "Control" => control_count += 1,
575                        "Meta" => meta_count += 1,
576                        "Alt" => {
577                            if try_consume_question() {
578                                ignore_alt_count += 1;
579                            } else {
580                                alt_count += 1
581                            }
582                        }
583                        "Shift" => {
584                            if try_consume_question() {
585                                ignore_shift_count += 1;
586                            } else {
587                                shift_count += 1;
588                            }
589                        }
590                        "AltR" | "ShiftR" | "MetaR" | "ControlR" => {
591                            bail(&mut p, "Right-side modifiers are not supported");
592                            break;
593                        }
594                        "AltGr" => {
595                            bail(&mut p, "AltGr cannot be used as a modifier");
596                            break;
597                        }
598                        "Command" | "Cmd" => {
599                            bail(
600                                &mut p,
601                                // \x20 equals to a space (needed to avoid the trailing \ eating
602                                // the indentation)
603                                &format!(
604                                    "{text} is not a cross-platform modifier\n\
605                                    Use cross-platform modifier names instead:\n\
606                                    \x20   ⌘ command -> Control\n\
607                                    \x20   ⌥ option -> Alt\n\
608                                    \x20   ^ control -> Meta\n\
609                                    \x20   ⇧ shift -> Shift"
610                                ),
611                            );
612                            break;
613                        }
614                        "Win" | "Windows" => {
615                            bail(
616                                &mut p,
617                                &format!(
618                                    "{text} is not a cross-platform modifier (Use `Meta` instead)"
619                                ),
620                            );
621                            break;
622                        }
623                        _ => key_count += 1,
624                    }
625                } else {
626                    key_count += 1;
627                }
628
629                state = State::NeedPlus;
630
631                if [
632                    alt_count,
633                    control_count,
634                    meta_count,
635                    shift_count,
636                    ignore_shift_count,
637                    ignore_alt_count,
638                ]
639                .into_iter()
640                .max()
641                .unwrap_or_default()
642                    > 1
643                {
644                    bail(&mut p, "Duplicated modifier in keyboard shortcut");
645                    break;
646                }
647                if shift_count > 0 && ignore_shift_count > 0 {
648                    bail(&mut p, "Cannot use both Shift and Shift? (remove one of them)");
649                    break;
650                }
651                if alt_count > 0 && ignore_alt_count > 0 {
652                    bail(&mut p, "Cannot use both Alt and Alt? (remove one of them)");
653                    break;
654                }
655                if key_count > 1 {
656                    bail(&mut p, "A keyboard shortcut can only contain one key (with modifiers)");
657                    break;
658                }
659
660                for _ in 0..consume_count {
661                    p.consume();
662                }
663                continue;
664            }
665            _ => {
666                let hint = if state == State::NeedKey {
667                    format!("\n(Consider using \"{}\")", p.peek().as_str())
668                } else {
669                    "".into()
670                };
671                bail(
672                    &mut p,
673                    &format!(
674                        "Expected '+', a string literal, or an identifier in the Keys namespace{hint}"
675                    ),
676                );
677                break;
678            }
679        }
680    }
681}
682
683#[cfg_attr(test, parser_test)]
684/// ```test,AtImageUrl
685/// @image-url("foo.png")
686/// @image-url("foo.png",)
687/// @image-url("foo.png", nine-slice(1 2 3 4))
688/// @image-url("foo.png", nine-slice(1))
689/// ```
690fn parse_image_url(p: &mut impl Parser) {
691    let mut p = p.start_node(SyntaxKind::AtImageUrl);
692    p.consume(); // "@"
693    p.consume(); // "image-url"
694    if !(p.expect(SyntaxKind::LParent)) {
695        return;
696    }
697    let peek = p.peek();
698    if peek.kind() != SyntaxKind::StringLiteral {
699        p.error("@image-url must contain a plain path as a string literal");
700        p.until(SyntaxKind::RParent);
701        return;
702    }
703    if !peek.as_str().starts_with('"') || !peek.as_str().ends_with('"') {
704        p.error("@image-url must contain a plain path as a string literal, without any '\\{}' expressions");
705        p.until(SyntaxKind::RParent);
706        return;
707    }
708    p.expect(SyntaxKind::StringLiteral);
709    if !p.test(SyntaxKind::Comma) {
710        if !p.test(SyntaxKind::RParent) {
711            p.error("Expected ')' or ','");
712            p.until(SyntaxKind::RParent);
713        }
714        return;
715    }
716    if p.test(SyntaxKind::RParent) {
717        return;
718    }
719    if p.peek().as_str() != "nine-slice" {
720        p.error("Expected 'nine-slice(...)' argument");
721        p.until(SyntaxKind::RParent);
722        return;
723    }
724    p.consume();
725    if !p.expect(SyntaxKind::LParent) {
726        p.until(SyntaxKind::RParent);
727        return;
728    }
729    let mut count = 0;
730    loop {
731        match p.peek().kind() {
732            SyntaxKind::RParent => {
733                if count != 1 && count != 2 && count != 4 {
734                    p.error("Expected 1 or 2 or 4 numbers");
735                }
736                p.consume();
737                break;
738            }
739            SyntaxKind::NumberLiteral => {
740                count += 1;
741                p.consume();
742            }
743            SyntaxKind::Comma | SyntaxKind::Colon => {
744                p.error("Arguments of nine-slice need to be separated by spaces");
745                p.until(SyntaxKind::RParent);
746                break;
747            }
748            _ => {
749                p.error("Expected number literal or ')'");
750                p.until(SyntaxKind::RParent);
751                break;
752            }
753        }
754    }
755    if !p.expect(SyntaxKind::RParent) {
756        p.until(SyntaxKind::RParent);
757    }
758}