Skip to main content

rbx_rsml/parser/
mod.rs

1use crate::types::Range;
2
3use crate::lexer::{RsmlLexer, TOKEN_KIND_CONSTRUCT_DELIMITERS, Token, TokenKind};
4use crate::list::TokenKindList;
5use crate::range_from_span::RangeFromSpan;
6use crate::{node_token_matches, token_kind_list};
7
8mod advance;
9mod datatype;
10mod declaration;
11mod parse_error;
12mod rule;
13pub mod types;
14
15use parse_error::{ParseError, ParseErrorMessage};
16pub use types::*;
17
18#[macro_export]
19macro_rules! node_token_matches {
20    ($node:ident, Some($( $name:ident )|*)) => {
21        matches!($node, Some($crate::parser::types::Node { token: $crate::lexer::SpannedToken (_, $( $crate::lexer::Token::$name )|*, _), .. }))
22    };
23
24    ($node:ident, $( $name:ident )|*) => {
25        matches!($node, $crate::parser::types::Node { token: $crate::lexer::SpannedToken (_, $( $crate::lexer::Token::$name )|*, _), .. })
26    };
27
28    ($node:ident, Some($( $name:ident($( $args:pat ),*) )|*)) => {
29        matches!($node, Some($crate::parser::types::Node { token: $crate::lexer::SpannedToken(_, $( $crate::lexer::Token::$name($( $args ),*) )|*, _), .. }))
30    };
31
32    ($node:ident, $( $name:ident($( $args:pat ),*) )|*) => {
33        matches!($node, $crate::parser::types::Node { token: $crate::lexer::SpannedToken(_, $( $crate::lexer::Token::$name($( $args ),*) )|*, _), .. })
34    };
35}
36
37pub struct RsmlParser<'a> {
38    pub lexer: RsmlLexer<'a>,
39    pub(crate) last_token_end: usize,
40
41    pub ast: Vec<Construct<'a>>,
42    pub ast_errors: AstErrors,
43
44    pub did_advance: bool,
45
46    pub directives: Directives,
47    pub(crate) pending_node: Option<Node<'a>>,
48    pub(crate) directives_phase_done: bool,
49}
50
51impl<'a> RsmlParser<'a> {
52    pub fn new(lexer: RsmlLexer<'a>) -> ParsedRsml<'a> {
53        let mut parser = Self {
54            lexer,
55            last_token_end: 0,
56
57            ast: Vec::new(),
58            ast_errors: AstErrors::new(),
59
60            did_advance: false,
61
62            directives: Directives::default(),
63            pending_node: None,
64            directives_phase_done: false,
65        };
66
67        parser.parse_directives();
68
69        parser.parse_loop(|parser, mut node| {
70            node = parser.parse_macro(node).handle_construct(&mut parser.ast)?;
71            node = parser
72                .parse_macro_call(node)
73                .handle_construct(&mut parser.ast)?;
74
75            node = parser
76                .parse_derive(node)
77                .handle_construct(&mut parser.ast)?;
78
79            node = parser
80                .parse_priority(node)
81                .handle_construct(&mut parser.ast)?;
82
83            node = parser.parse_tween(node).handle_construct(&mut parser.ast)?;
84
85            node = parser
86                .parse_static_token_assignment(node)
87                .handle_construct(&mut parser.ast)?;
88
89            node = parser
90                .parse_token_assignment(node)
91                .handle_construct(&mut parser.ast)?;
92
93            node = parser
94                .parse_property_assignment_or_rule_scope(node)
95                .handle_construct(&mut parser.ast)?;
96            node = parser
97                .parse_rule_scope_selector_begin(node)
98                .handle_construct(&mut parser.ast)?;
99
100            node = parser.parse_none(node).handle_construct(&mut parser.ast)?;
101
102            Some(node)
103        });
104
105        ParsedRsml {
106            ast: parser.ast,
107            ast_errors: parser.ast_errors,
108            directives: parser.directives,
109            rope: parser.lexer.rope,
110        }
111    }
112
113    pub fn from_source(source: &'a str) -> ParsedRsml<'a> {
114        Self::new(RsmlLexer::new(source))
115    }
116
117    pub fn range_from_span(&self, span: (usize, usize)) -> Range {
118        Range::from_span(&self.lexer.rope, span)
119    }
120
121    fn parse_assignment(&mut self, node: Node<'a>) -> Parsed<'a> {
122        let middle_node =
123            match self.advance_until(token_kind_list![Equals], &TOKEN_KIND_CONSTRUCT_DELIMITERS) {
124                Some(Ok(node)) => node,
125                Some(Err(node)) => return Parsed(Some(node), None),
126                None => return Parsed(None, None),
127            };
128
129        let left_node = node;
130
131        let node = self.advance_without_flags();
132        self.did_advance = true;
133
134        let (node_status, body_nodes) = self.parse_datatype(node, TOKEN_KIND_CONSTRUCT_DELIMITERS);
135        let body_nodes = body_nodes.map(|x| Box::new(x));
136
137        let terminator = match node_status {
138            NodeStatus::Exists => match self.advance_until(
139                token_kind_list![SemiColon],
140                &TOKEN_KIND_CONSTRUCT_DELIMITERS,
141            ) {
142                Some(Ok(node)) => node,
143                Some(Err(node)) => {
144                    return Parsed(
145                        Some(node),
146                        Some(Construct::Assignment {
147                            left: left_node,
148                            middle: Some(middle_node),
149                            right: body_nodes,
150                            terminator: None,
151                        }),
152                    );
153                }
154                None => {
155                    return Parsed(
156                        None,
157                        Some(Construct::Assignment {
158                            left: left_node,
159                            middle: Some(middle_node),
160                            right: body_nodes,
161                            terminator: None,
162                        }),
163                    );
164                }
165            },
166
167            NodeStatus::Err(node) => {
168                if node_token_matches!(node, SemiColon) {
169                    node
170                } else {
171                    let construct = Construct::Assignment {
172                        left: left_node,
173                        middle: Some(middle_node),
174                        right: body_nodes,
175                        terminator: None,
176                    };
177
178                    self.ast_errors.push(
179                        ParseError::MissingToken {
180                            msg: Some(ParseErrorMessage::Expected(TokenKind::SemiColon.name())),
181                        },
182                        self.range_from_span(clamp_span_to_end(construct.end())),
183                    );
184
185                    return Parsed(Some(node), Some(construct));
186                }
187            }
188
189            NodeStatus::None => {
190                let construct = Construct::Assignment {
191                    left: left_node,
192                    middle: Some(middle_node),
193                    right: body_nodes,
194                    terminator: None,
195                };
196
197                self.ast_errors.push(
198                    ParseError::MissingToken {
199                        msg: Some(ParseErrorMessage::Expected(TokenKind::SemiColon.name())),
200                    },
201                    self.range_from_span(clamp_span_to_end(construct.end())),
202                );
203
204                return Parsed(None, Some(construct));
205            }
206        };
207
208        Parsed(
209            self.advance(),
210            Some(Construct::Assignment {
211                left: left_node,
212                middle: Some(middle_node),
213                right: body_nodes,
214                terminator: Some(terminator),
215            }),
216        )
217    }
218
219    pub(crate) fn parse_static_token_assignment(&mut self, node: Node<'a>) -> Parsed<'a> {
220        if !node_token_matches!(node, StaticTokenIdentifier(_)) {
221            return Parsed(Some(node), None);
222        };
223        self.parse_assignment(node)
224    }
225
226    pub(crate) fn parse_token_assignment(&mut self, node: Node<'a>) -> Parsed<'a> {
227        if !node_token_matches!(node, TokenIdentifier(_)) {
228            return Parsed(Some(node), None);
229        };
230        self.parse_assignment(node)
231    }
232
233    pub(crate) fn parse_none(&mut self, node: Node<'a>) -> Parsed<'a> {
234        if !node_token_matches!(node, None) {
235            return Parsed(Some(node), None);
236        };
237
238        Parsed(self.advance(), Some(Construct::None { node }))
239    }
240
241    pub(crate) fn parse_loop<F: Fn(&mut Self, Node<'a>) -> Option<Node<'a>>>(
242        &mut self,
243        routine: F,
244    ) -> Option<Node<'a>> {
245        let mut node = self.advance_without_flags().update_last_token_end(self)?;
246        let token = &node.token;
247
248        let mut error_span: Option<(usize, usize)> = if matches!(token.value(), Token::Error) {
249            Some((token.start(), token.end()))
250        } else {
251            None
252        };
253
254        loop {
255            let Some(next_node) = routine(self, node) else {
256                break;
257            };
258            node = next_node;
259
260            if self.did_advance {
261                self.did_advance = false;
262
263                if let Some((error_span_start, error_span_end)) = error_span {
264                    self.ast_errors.push(
265                        ParseError::UnexpectedTokens { msg: None },
266                        self.range_from_span((error_span_start, error_span_end)),
267                    );
268                }
269            } else {
270                let token = &node.token;
271
272                if let Some((error_span_start, _)) = error_span {
273                    error_span = Some((error_span_start, token.end()))
274                } else {
275                    error_span = Some((token.start(), token.end()))
276                }
277
278                let Some(next_node) = self.advance_without_flags().update_last_token_end(self)
279                else {
280                    break;
281                };
282
283                node = next_node;
284            }
285        }
286
287        if let Some((error_span_start, error_span_end)) = error_span {
288            self.ast_errors.push(
289                ParseError::UnexpectedTokens { msg: None },
290                self.range_from_span((error_span_start, error_span_end)),
291            );
292        }
293
294        None
295    }
296
297    #[cfg(test)]
298    pub fn parse_source(source: &'a str) -> ParsedRsml<'a> {
299        let lexer = crate::lexer::RsmlLexer::new(source);
300        Self::new(lexer)
301    }
302
303    pub(crate) fn parse_loop_inner<F: FnMut(&mut Self, Node<'a>) -> Option<(Node<'a>, bool)>>(
304        &mut self,
305        mut node: Node<'a>,
306        mut routine: F,
307    ) -> (Option<Node<'a>>, ParseEndedReason) {
308        let last_did_advance = self.did_advance;
309        self.did_advance = false;
310
311        let mut error_span: Option<(usize, usize)> = None;
312
313        loop {
314            let Some(parsed) = routine(self, node) else {
315                self.did_advance = last_did_advance;
316                return (None, ParseEndedReason::Eof);
317            };
318            node = parsed.0;
319
320            if self.did_advance {
321                self.did_advance = false;
322
323                if let Some((error_span_start, error_span_end)) = error_span {
324                    self.ast_errors.push(
325                        ParseError::UnexpectedTokens { msg: None },
326                        self.range_from_span((error_span_start, error_span_end)),
327                    );
328                }
329
330                if parsed.1 {
331                    return (Some(node), ParseEndedReason::Manual);
332                }
333            } else {
334                if parsed.1 {
335                    if let Some((error_span_start, error_span_end)) = error_span {
336                        self.ast_errors.push(
337                            ParseError::UnexpectedTokens { msg: None },
338                            self.range_from_span((error_span_start, error_span_end)),
339                        );
340                    }
341
342                    return (Some(node), ParseEndedReason::Manual);
343                }
344
345                let token = &node.token;
346                if let Some((error_span_start, _)) = error_span {
347                    error_span = Some((error_span_start, token.end()))
348                } else {
349                    error_span = Some((token.start(), token.end()))
350                }
351
352                let Some(next_node) = self.advance_without_flags().update_last_token_end(self)
353                else {
354                    break;
355                };
356                node = next_node;
357            }
358        }
359
360        if let Some((error_span_start, error_span_end)) = error_span {
361            self.ast_errors.push(
362                ParseError::UnexpectedTokens { msg: None },
363                self.range_from_span((error_span_start, error_span_end)),
364            );
365        }
366
367        self.did_advance = last_did_advance;
368
369        (Some(node), ParseEndedReason::Eof)
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use crate::parser::*;
376    use crate::compiler::RsmlCompiler;
377
378    macro_rules! parser_test {
379        ($name:ident, $source:expr) => {
380            #[test]
381            fn $name() {
382                let parsed = RsmlParser::parse_source($source);
383                insta::assert_debug_snapshot!(parsed.ast);
384            }
385
386            paste::paste! {
387                #[test]
388                fn [<compiler_ $name>]() {
389                    let parsed = RsmlParser::parse_source($source);
390                    let compiled = RsmlCompiler::new(parsed);
391                    insta::assert_debug_snapshot!(compiled);
392                }
393            }
394        };
395    }
396
397    #[test]
398    fn unary_minus_in_udim2_expression() {
399        let source = r#"$Size = udim2(-20px + 100%, -20px + 100%);"#;
400        let parsed = RsmlParser::parse_source(source);
401
402        assert!(parsed.ast_errors.0.is_empty(), "Expected no parse errors, got: {:?}", parsed.ast_errors.0);
403        insta::assert_debug_snapshot!(parsed.ast);
404    }
405
406    parser_test!(derive_string, r#"@derive "some-module";"#);
407    parser_test!(derive_missing_semicolon, r#"@derive "module""#);
408    parser_test!(derive_missing_body, r#"@derive"#);
409    parser_test!(priority_number, r#"@priority 10;"#);
410    parser_test!(priority_missing_semicolon, r#"@priority 10"#);
411    parser_test!(tween_simple, r#"@tween MyTween 0.5;"#);
412    parser_test!(tween_string_value, r#"@tween Slide "ease-in";"#);
413    parser_test!(tween_missing_name, r#"@tween ;"#);
414    parser_test!(tween_missing_semicolon, r#"@tween MyTween 0.5"#);
415
416    parser_test!(assign_property_string, r#"Text = "hello";"#);
417    parser_test!(assign_property_number, r#"Size = 42;"#);
418    parser_test!(assign_property_boolean, r#"Visible = true;"#);
419    parser_test!(assign_property_nil, r#"Parent = nil;"#);
420    parser_test!(assign_property_missing_value, r#"Text = ;"#);
421    parser_test!(assign_property_missing_semicolon, r#"Text = "hello""#);
422    parser_test!(assign_token, r#"$Size = 100;"#);
423    parser_test!(assign_token_annotated_table, r#"$Size = udim2(1, 0, 1, 0);"#);
424    parser_test!(assign_token_missing_semicolon, r#"$Size = 100"#);
425    parser_test!(assign_static_token, r#"$!Padding = 10px;"#);
426    parser_test!(assign_static_token_missing_value, r#"$!Padding = ;"#);
427
428    parser_test!(value_number_scale, r#"Size = 100%;"#);
429    parser_test!(value_number_offset, r#"Size = 20px;"#);
430    parser_test!(value_string_double, r#"Text = "hello world";"#);
431    parser_test!(value_string_single, r#"Text = 'hello world';"#);
432    parser_test!(value_string_multi, r#"Text = [[multi line]];"#);
433    parser_test!(value_color_hex, r#"Color = #ff00ff;"#);
434    parser_test!(value_color_tailwind, r#"Color = tw:red:500;"#);
435    parser_test!(value_color_css, r#"Color = css:tomato;"#);
436    parser_test!(value_color_brick, r#"Color = bc:red;"#);
437    parser_test!(value_rbx_asset, r#"Image = rbxassetid://12345;"#);
438    parser_test!(value_rbx_content, r#"Image = contentid://12345;"#);
439    parser_test!(value_enum, r#"SortOrder = Enum.SortOrder.LayoutOrder;"#);
440    parser_test!(value_enum_missing_variant, r#"SortOrder = Enum.SortOrder;"#);
441
442    parser_test!(annotated_table_udim2, r#"$Size = udim2(1, 0, 1, 0);"#);
443    parser_test!(annotated_table_no_args, r#"$Val = empty();"#);
444    parser_test!(annotated_table_nested, r#"$Val = outer(inner(1, 2));"#);
445    parser_test!(annotated_table_missing_close, r#"$Val = udim2(1, 0;"#);
446    parser_test!(table_bare, r#"$Val = (1, 2, 3);"#);
447    parser_test!(table_empty, r#"$Val = ();"#);
448    parser_test!(table_nested, r#"$Val = ((1, 2), (3, 4));"#);
449    parser_test!(table_missing_close, r#"$Val = (1, 2"#);
450
451    parser_test!(math_add, r#"$Val = 10 + 20;"#);
452    parser_test!(math_sub, r#"$Val = 10 - 5;"#);
453    parser_test!(math_mult, r#"$Val = 10 * 5;"#);
454    parser_test!(math_div, r#"$Val = 10 / 5;"#);
455    parser_test!(math_floor_div, r#"$Val = 10 // 3;"#);
456    parser_test!(math_mod, r#"$Val = 10 % 3;"#);
457    parser_test!(math_pow, r#"$Val = 2 ^ 8;"#);
458    parser_test!(math_precedence_add_mult, r#"$Val = 1 + 2 * 3;"#);
459    parser_test!(math_precedence_mult_add, r#"$Val = 1 * 2 + 3;"#);
460    parser_test!(math_chained_add_sub, r#"$Val = 1 + 2 - 3 + 4;"#);
461    parser_test!(unary_minus_simple, r#"$Val = -10;"#);
462    parser_test!(unary_minus_in_expression, r#"$Val = -10 + 20;"#);
463    parser_test!(math_udim_mixed, r#"$Size = 50% + 10px;"#);
464    parser_test!(math_missing_right_operand, r#"$Val = 10 +;"#);
465
466    parser_test!(rule_identifier, r#"Frame { }"#);
467    parser_test!(rule_name_selector, r#"#MyFrame { }"#);
468    parser_test!(rule_tag_selector, r#".tagged { }"#);
469    parser_test!(rule_state_selector, r#":hover { }"#);
470    parser_test!(rule_pseudo_selector, r#"::UIPadding { }"#);
471    parser_test!(rule_children_selector, r#"Frame > TextLabel { }"#);
472    parser_test!(rule_descendants_selector, r#"Frame >> TextLabel { }"#);
473    parser_test!(rule_comma_selectors, r#"Frame, TextLabel { }"#);
474    parser_test!(rule_comma_three, r#"Frame, TextLabel, ImageLabel { }"#);
475    parser_test!(rule_compound, r#"Frame .tag :hover { }"#);
476    parser_test!(rule_with_assignment, r#"Frame { Size = 100; }"#);
477    parser_test!(rule_with_multiple_assignments, "Frame {\n    Size = 100;\n    Visible = true;\n}");
478    parser_test!(rule_nested, r#"Frame { TextLabel { Text = "hi"; } }"#);
479    parser_test!(rule_deeply_nested, r#".tag { :hover { Color = #f00; } }"#);
480    parser_test!(rule_missing_close_brace, r#"Frame { Size = 100;"#);
481    parser_test!(rule_children_missing_part, r#"Frame > { }"#);
482    parser_test!(rule_macro_call_selector, r#"sel!(10px) { Size = 100; }"#);
483    parser_test!(rule_macro_call_comma, r#"sel!(1), Frame { }"#);
484
485    parser_test!(macro_construct_return, r#"@macro MyMacro -> Construct { Size = 100; }"#);
486    parser_test!(macro_args_construct_return, r#"@macro MyMacro(&v) -> Construct { Size = &v; }"#);
487    parser_test!(macro_datatype_return, r#"@macro MyColor -> Datatype { #ff0000 }"#);
488    parser_test!(macro_datatype_return_args, r#"@macro Scale(&x) -> Datatype { &x }"#);
489    parser_test!(macro_selector_return, r#"@macro MySel -> Selector { Frame .tag }"#);
490    parser_test!(macro_selector_return_args, r#"@macro MySel(&c) -> Selector { Frame .tag :hover }"#);
491    parser_test!(macro_nested_rule, r#"@macro Theme(&c) -> Construct { Frame { Color = &c; } }"#);
492    parser_test!(macro_empty_body, r#"@macro MyMacro -> Construct { }"#);
493    parser_test!(macro_missing_name, r#"@macro { }"#);
494    parser_test!(macro_missing_body, r#"@macro MyMacro"#);
495    parser_test!(macro_missing_close_brace, r#"@macro M -> Construct { Size = 1;"#);
496    parser_test!(macro_invalid_return_type, r#"@macro M -> Invalid { }"#);
497    parser_test!(macro_args_missing_comma, r#"@macro M(&a &b) -> Construct { }"#);
498
499    parser_test!(macro_call_no_args, r#"MyMacro!();"#);
500    parser_test!(macro_call_with_args, r#"MyMacro!(10px, 20px);"#);
501    parser_test!(macro_call_complex_args, r#"Apply!(#ff0000, 10px, "hello");"#);
502    parser_test!(macro_call_missing_semicolon, r#"MyMacro!()"#);
503    parser_test!(macro_call_missing_close_paren, r#"MyMacro!(10px;"#);
504
505    parser_test!(builtin_padding_one_arg, r#"Frame { Padding!(10px); }"#);
506    parser_test!(builtin_padding_two_args, r#"Frame { Padding!(10px, 20px); }"#);
507    parser_test!(builtin_padding_three_args, r#"Frame { Padding!(10px, 20px, 30px); }"#);
508    parser_test!(builtin_padding_four_args, r#"Frame { Padding!(10px, 20px, 30px, 40px); }"#);
509    parser_test!(builtin_corner_radius, r#"Frame { CornerRadius!(8px); }"#);
510    parser_test!(builtin_scale, r#"Frame { Scale!(1.5); }"#);
511    parser_test!(macro_call_math_arg, r#"Frame { Padding!(0% + .5); }"#);
512
513    parser_test!(comment_before_assign, "-- comment\nSize = 100;");
514    parser_test!(comment_multi_before_assign, r#"--[[comment]] Size = 100;"#);
515    parser_test!(comment_multi_nested, r#"--[==[comment]==] Size = 100;"#);
516    parser_test!(comment_leading_trivia, "-- a\n-- b\nSize = 100;");
517
518    parser_test!(directive_nobuiltins_alone, "--!nobuiltins");
519    parser_test!(directive_nobuiltins_then_code, "--!nobuiltins\nSize = 100;");
520    parser_test!(directive_nobuiltins_blocks_builtin_expansion, "--!nobuiltins\nFrame { Padding!(10px); }");
521    parser_test!(directive_strict_alone, "--!strict");
522    parser_test!(directive_nonstrict_alone, "--!nonstrict");
523    parser_test!(directive_after_comment, "-- preface\n--!nobuiltins\nSize = 100;");
524    parser_test!(directive_unknown, "--!foo\nSize = 100;");
525    parser_test!(directive_empty, "--!\nSize = 100;");
526    parser_test!(directive_after_code, "Size = 1;\n--!nobuiltins\nSize = 2;");
527
528    #[test]
529    fn directive_sets_nobuiltins_flag() {
530        let parsed = RsmlParser::parse_source("--!nobuiltins\nSize = 100;");
531        assert!(parsed.directives.nobuiltins);
532    }
533
534    #[test]
535    fn no_directive_leaves_flag_unset() {
536        let parsed = RsmlParser::parse_source("Size = 100;");
537        assert!(!parsed.directives.nobuiltins);
538    }
539
540    #[test]
541    fn directive_sets_strict_language_mode() {
542        use crate::types::LanguageMode;
543        let parsed = RsmlParser::parse_source("--!strict\nSize = 100;");
544        assert_eq!(parsed.directives.language_mode, Some(LanguageMode::Strict));
545    }
546
547    #[test]
548    fn directive_sets_nonstrict_language_mode() {
549        use crate::types::LanguageMode;
550        let parsed = RsmlParser::parse_source("--!nonstrict\nSize = 100;");
551        assert_eq!(parsed.directives.language_mode, Some(LanguageMode::Nonstrict));
552    }
553
554    #[test]
555    fn no_directive_leaves_language_mode_unset() {
556        let parsed = RsmlParser::parse_source("Size = 100;");
557        assert_eq!(parsed.directives.language_mode, None);
558    }
559
560    #[test]
561    fn directive_after_code_emits_error() {
562        let parsed = RsmlParser::parse_source("Size = 1;\n--!nobuiltins");
563        assert!(!parsed.directives.nobuiltins);
564        assert!(parsed.ast_errors.0.iter().any(|d| d.code == "DIRECTIVE_NOT_AT_TOP"));
565    }
566
567    #[test]
568    fn unknown_directive_emits_error() {
569        let parsed = RsmlParser::parse_source("--!foo\nSize = 1;");
570        assert!(parsed.ast_errors.0.iter().any(|d| d.code == "UNKNOWN_DIRECTIVE"));
571    }
572
573    #[test]
574    fn empty_directive_emits_error() {
575        let parsed = RsmlParser::parse_source("--!\nSize = 1;");
576        assert!(parsed.ast_errors.0.iter().any(|d| d.code == "EMPTY_DIRECTIVE"));
577    }
578
579    parser_test!(query_selector, r#"@media { }"#);
580    parser_test!(query_selector_unknown, r#"@foobar { }"#);
581
582    parser_test!(empty_source, r#""#);
583    parser_test!(multiple_top_level, "@priority 5;\nFrame { Size = 100; }");
584    parser_test!(full_stylesheet, r#"
585@derive "base";
586@priority 5;
587@tween Fade 0.3;
588
589$!PrimaryColor = #3498db;
590$Padding = 10px;
591
592@macro Highlight(&color) -> Construct {
593    BackgroundColor3 = &color;
594}
595
596Frame {
597    BackgroundColor3 = $!PrimaryColor;
598    Size = udim2(1, -$Padding * 2, 1, -$Padding * 2);
599
600    TextLabel {
601        Text = "Hello";
602        TextColor3 = css:white;
603    }
604
605    :hover {
606        BackgroundColor3 = tw:blue:600;
607    }
608}
609
610#Sidebar, .panel {
611    Size = udim2(0, 200px, 1, 0);
612}
613"#);
614    parser_test!(macro_def_and_call, "@macro P(&v) -> Construct { $!P = &v; }\nP!(10px);");
615    parser_test!(
616        macro_user_nested_expansion,
617        "@macro Inner(&v) -> Construct { ::UIPadding { PaddingTop = &v; } }\n@macro Outer(&v) -> Construct { Inner!(&v); }\nFrame { Outer!(10px); }"
618    );
619    parser_test!(
620        macro_recursion_guard,
621        "@macro Recur() -> Construct { Recur!(); }\nFrame { Recur!(); }"
622    );
623    parser_test!(
624        macro_overload_cross_call_not_blocked,
625        "@macro Foo() -> Construct { Foo!(10px); }\n@macro Foo(&v) -> Construct { ::Inner { X = &v; } }\nFrame { Foo!(); }"
626    );
627    parser_test!(
628        macro_overload_by_arg_count,
629        "@macro Set(&a) -> Construct { ::Inner { X = &a; } }\n@macro Set(&a, &b) -> Construct { ::Inner { Y = &b; } }\nFrame { Set!(1px); Set!(2px, 3px); }"
630    );
631    parser_test!(
632        macro_selector_expansion,
633        "@macro Foo -> Selector { TextButton }\nFoo!(), Frame { }"
634    );
635    parser_test!(
636        macro_selector_overload,
637        "@macro Sel -> Selector { A }\n@macro Sel(&x) -> Selector { B }\nSel!() { }\nSel!(1) { }"
638    );
639    parser_test!(
640        macro_selector_recursion_guard,
641        "@macro Loop -> Selector { Loop!() }\nLoop!() { }"
642    );
643    parser_test!(
644        macro_selector_overload_cross_call_not_blocked,
645        "@macro Sel -> Selector { Sel!(1) }\n@macro Sel(&x) -> Selector { TextButton }\nSel!() { }"
646    );
647    parser_test!(
648        macro_selector_recursion_inline_comma,
649        "@macro Foo -> Selector { TextButton, Foo!() }\nFoo!(), Frame { }"
650    );
651    parser_test!(
652        macro_selector_undefined_dropped,
653        "Missing!(), Frame { }"
654    );
655    parser_test!(
656        macro_indirect_recursion_typechecker_error,
657        "@macro A() -> Construct { B!(); }\n@macro B() -> Construct { A!(); }\nFrame { A!(); }"
658    );
659}