diff --git a/crates/mq-formatter/src/formatter.rs b/crates/mq-formatter/src/formatter.rs index c63dc422a..e8631eef7 100644 --- a/crates/mq-formatter/src/formatter.rs +++ b/crates/mq-formatter/src/formatter.rs @@ -376,10 +376,19 @@ impl Formatter { self.append_space(); } + // Check for 'do' keyword or colon + let do_index = Self::find_token_position(node, |kind| matches!(kind, mq_lang::TokenKind::Do)); let colon_index = Self::find_token_position(node, |kind| matches!(kind, mq_lang::TokenKind::Colon)); + let uses_do_syntax = do_index.is_some(); - // If there's no colon, split before the right parenthesis - let expr_index = self.calculate_split_position(node, 0); + // If there's no colon or do, split before the right parenthesis + let expr_index = if let Some(index) = do_index.or(colon_index) { + index + } else { + Self::find_token_position(node, |kind| matches!(kind, mq_lang::TokenKind::RParen)) + .map(|index| index + 1) + .unwrap_or(0) + }; node.children.iter().take(expr_index).for_each(|child| { self.format_node( @@ -394,11 +403,17 @@ impl Formatter { let mut expr_nodes = node.children.iter().skip(expr_index).peekable(); - // Format colon if it exists - if colon_index.is_some() - && let Some(colon_node) = expr_nodes.next() + // Format colon or do keyword if it exists + if (colon_index.is_some() || do_index.is_some()) + && let Some(separator_node) = expr_nodes.next() { - self.format_colon_with_spacing(colon_node, &mut expr_nodes, block_indent_level + 1); + if uses_do_syntax { + // Format 'do' keyword with standardized spacing + self.format_do_with_spacing(separator_node, &mut expr_nodes); + } else { + // Format colon with spacing + self.format_colon_with_spacing(separator_node, &mut expr_nodes, block_indent_level + 1); + } } let block_indent_level = if is_prev_pipe { @@ -803,17 +818,28 @@ impl Formatter { let indent_adjustment = self.calculate_indent_adjustment(); - // Find the colon position - let colon_pos = Self::find_token_position(node, |kind| matches!(kind, mq_lang::TokenKind::Colon)).unwrap_or(0); + // Check for 'do' keyword or colon + let do_pos = Self::find_token_position(node, |kind| matches!(kind, mq_lang::TokenKind::Do)); + let colon_pos = Self::find_token_position(node, |kind| matches!(kind, mq_lang::TokenKind::Colon)); + let uses_do_syntax = do_pos.is_some(); + let separator_pos = do_pos.or(colon_pos).unwrap_or(0); // Format arguments (lparen, value, rparen) - for child in node.children.iter().take(colon_pos) { + for child in node.children.iter().take(separator_pos) { self.format_node(mq_lang::Shared::clone(child), 0); } - // Format colon - if let Some(colon) = node.children.get(colon_pos) { - self.format_node(mq_lang::Shared::clone(colon), 0); + // Format colon or do keyword + let mut remaining_children = node.children.iter().skip(separator_pos).peekable(); + + if let Some(separator) = remaining_children.next() { + if uses_do_syntax { + // Format 'do' keyword with standardized spacing + self.format_do_with_spacing(separator, &mut remaining_children); + } else { + // Format colon + self.format_node(mq_lang::Shared::clone(separator), 0); + } } // Calculate indent level for match arms (similar to format_if) @@ -827,7 +853,7 @@ impl Formatter { let end_indent_level = if is_prev_pipe { indent_level + 1 } else { indent_level } + indent_adjustment; // Format match arms and end - let remaining_children: Vec<_> = node.children.iter().skip(colon_pos + 1).collect(); + let remaining_children: Vec<_> = remaining_children.collect(); // Check if this is a multiline match (first match arm has new line) let is_multiline = remaining_children @@ -1345,6 +1371,31 @@ impl Formatter { } } + /// Formats a 'do' keyword with standardized spacing. + /// Adds space before the 'do' keyword, formats it, and adds space after + /// if the next node doesn't have a newline and is not a MatchArm. + fn format_do_with_spacing<'a, I>( + &mut self, + do_node: &mq_lang::Shared, + remaining: &mut std::iter::Peekable, + ) where + I: Iterator>, + { + // Add space before 'do' keyword + self.append_space(); + + // Format the 'do' keyword + self.format_node(mq_lang::Shared::clone(do_node), 0); + + // Add space after 'do' if next node doesn't have newline and is not a MatchArm + if let Some(next) = remaining.peek() + && !next.has_new_line() + && !matches!(next.kind, mq_lang::CstNodeKind::MatchArm) + { + self.append_space(); + } + } + /// Escapes control characters in a string, preserving existing valid escape sequences fn escape_string(s: &str) -> String { let mut result = String::with_capacity(s.len() * 2); @@ -2292,6 +2343,115 @@ end"#, else do test2 end +"# + )] + #[case::while_do_end_oneline("while(x > 0) do x - 1 end", "while (x > 0) do x - 1 end")] + #[case::while_do_end_multiline( + r#"while(x > 0) do +let x = x - 1 | x +end"#, + r#"while (x > 0) do + let x = x - 1 | x +end +"# + )] + #[case::while_do_end_with_break( + r#"while(x < 10) do +let x = x + 1 +| if(x == 3): +break +else: +x +end"#, + r#"while (x < 10) do + let x = x + 1 + | if (x == 3): + break + else: + x +end +"# + )] + #[case::foreach_do_end_oneline( + "foreach(item, arr) do process(item) end", + "foreach (item, arr) do process(item) end" + )] + #[case::foreach_do_end_multiline( + r#"foreach(x, array(1, 2, 3)) do +add(x, 1) +end"#, + r#"foreach (x, array(1, 2, 3)) do + add(x, 1) +end +"# + )] + #[case::foreach_do_end_with_continue( + r#"foreach(x, array(1, 2, 3, 4, 5)) do +if(x == 3): +continue +else: +x + 10 +end"#, + r#"foreach (x, array(1, 2, 3, 4, 5)) do + if (x == 3): + continue + else: + x + 10 +end +"# + )] + #[case::foreach_do_end_nested( + r#"foreach(row, arr) do +foreach(x, row) do +x * 2 +end +end"#, + r#"foreach (row, arr) do + foreach (x, row) do + x * 2 + end +end +"# + )] + #[case::match_do_end_oneline( + "match(x) do | 1: \"one\" | 2: \"two\" | _: \"other\" end", + "match (x) do | 1: \"one\" | 2: \"two\" | _: \"other\" end" + )] + #[case::match_do_end_multiline( + r#"match(2) do +| 1: "one" +| 2: "two" +| _: "other" +end"#, + r#"match (2) do + | 1: "one" + | 2: "two" + | _: "other" +end +"# + )] + #[case::match_do_end_type_pattern( + r#"match(array(1, 2, 3)) do +| :array: "is_array" +| :number: "is_number" +| _: "other" +end"#, + r#"match (array(1, 2, 3)) do + | :array: "is_array" + | :number: "is_number" + | _: "other" +end +"# + )] + #[case::match_do_end_with_guard( + r#"match(x) do +| n if(n > 0): "positive" +| _: "non-positive" +end"#, + r#"match (x) do + | n if (n > 0): "positive" + | _: "non-positive" +end "# )] fn test_format(#[case] code: &str, #[case] expected: &str) { diff --git a/crates/mq-lang/builtin.mq b/crates/mq-lang/builtin.mq index c3e1610c2..f237f3c9a 100644 --- a/crates/mq-lang/builtin.mq +++ b/crates/mq-lang/builtin.mq @@ -498,7 +498,10 @@ end # Executes an expression n times and returns an array of results. macro times(t_n, t_expr) do quote do - foreach (_, range(0, unquote(t_n) - 1)): unquote(t_expr); + if (unquote(t_n) == 0): + [] + else: + foreach (_, range(0, unquote(t_n) - 1)): unquote(t_expr); end end diff --git a/crates/mq-lang/modules/module_tests.mq b/crates/mq-lang/modules/module_tests.mq index 359b62dc3..5724f14a5 100644 --- a/crates/mq-lang/modules/module_tests.mq +++ b/crates/mq-lang/modules/module_tests.mq @@ -53,7 +53,7 @@ import "csv" | def test_toml_to_json(): let result = toml::toml_to_json(toml::toml_parse(toml_input)) - | assert_eq(result, "{\"package\":{\"name\":\"test-package\",\"authors\":[\"Test Author \"],\"description\":\"A test TOML configuration file\",\"edition\":\"2021\",\"license\":\"MIT\",\"version\":\"1.0.0\"},\"dependencies\":{\"clap\":\"4.0\",\"serde\":\"1.0\",\"tokio\":{\"version\":\"1.0\",\"features\":[\"full\"]}},\"features\":{\"json\":[\"serde_json\"],\"yaml\":[\"serde_yaml\"],\"default\":[\"json\"]},\"dev-dependencies\":{\"assert_cmd\":\"2.0\",\"predicates\":\"3.0\"},\"bin\":[{\"name\":\"test-cli\",\"path\":\"src/main.rs\"}],\"build-dependencies\":{\"cc\":\"1.0\"},\"profile\":{\"release\":{\"codegen-units\":1,\"lto\":true,\"opt-level\":3}},\"workspace\":{\"members\":[\"crate1\",\"crate2\",\"subdir/crate3\"]},\"metadata\":{\"docs\":{\"rs\":{\"all-features\":true,\"rustdoc-args\":[\"--cfg\",\"docsrs\"]}}},\"config\":{\"debug\":true,\"max_connections\":100,\"timeout\":30,\"database\":{\"name\":\"testdb\",\"host\":\"localhost\",\"password\":\"secret\",\"port\":5432,\"user\":\"admin\"},\"server\":{\"host\":\"0.0.0.0\",\"port\":8080,\"workers\":4}},\"database\":{\"connection\":[{\"server\":\"192.168.1.1\",\"enabled\":true,\"connection_max\":5000,\"ports\":[8001,8001,8002]},{\"server\":\"192.168.1.2\",\"enabled\":false,\"connection_max\":300,\"ports\":[8001]}]},\"servers\":[{\"name\":\"alpha\",\"ip\":\"10.0.0.1\",\"role\":\"frontend\"},{\"name\":\"beta\",\"ip\":\"10.0.0.2\",\"role\":\"backend\"}],\"logging\":{\"level\":\"info\",\"format\":\"json\",\"file\":{\"path\":\"/var/log/app.log\",\"max_size\":\"10MB\",\"rotate\":true},\"console\":{\"colors\":true,\"enabled\":true,\"test_booleans\":[true,false,true],\"test_floats\":[3.14,2.71,1.41],\"test_numbers\":[1,2,3,42],\"test_strings\":[\"hello\",\"world\",\"test\"]}},\"mixed_data\":{\"array_value\":[1,2,3],\"boolean_value\":true,\"date_value\":\"2024-01-01T00:00:00Z\",\"float_value\":3.14159,\"integer_value\":42,\"string_value\":\"test string\",\"nested\":{\"key1\":\"value1\",\"key2\":\"value2\",\"bin_number\":0,\"b11010110\":null,\"float_with_exponent\":5,\"e\":22,\"float_with_underscore\":224617.445991,\"hex_number\":0,\"xDEADBEEF\":null,\"int_with_underscore\":1000000,\"oct_number\":0,\"o755\":null,\"local_date\":\"1979-05-27\",\"local_datetime\":\"1979-05-27T07:32:00\",\"local_time\":\"07:32:00\",\"offset_datetime\":\"1979-05-27T07:32:00-08:00\",\"heterogeneous_array\":[1,2,\"a\",\"b\",\"c\"],\"nested_array\":[\"gamma\",\"delta\",1,2],\"inline_table\":{\"x\":1,\"y\":2},\"nested_inline\":{\"person\":{\"name\":\"John\",\"age\":30}},\"infinity\":9223372036854775807,\"negative_infinity\":-9223372036854775808,\"not_a_number\":NaN,\"empty_array\":[],\"empty_string\":\"\",\"\":null,\"russian_comment\":\"Привет мир\"}},\"products\":[{\"name\":\"Hammer\",\"sku\":738594937},{\"name\":\"Nail\",\"sku\":284758393,\"color\":\"gray\"}],\"tool\":{\"name\":\"cargo\",\"version\":\"1.70.0\",\"features\":{\"default\":[\"std\"],\"no-std\":[\"core\"],\"std\":[]},\"settings\":{\"format\":\"json\",\"compression\":true}},\"127.0.0.1\":\"localhost\",\"barke_key\":\"value\",\"character encoding\":\"UTF-8\",\"quoted_key\":\"value\",\"ʎǝʞ\":\"upside down key\"}") + | assert_eq(result, "{\"package\":{\"name\":\"test-package\",\"authors\":[\"Test Author \"],\"description\":\"A test TOML configuration file\",\"edition\":\"2021\",\"license\":\"MIT\",\"version\":\"1.0.0\"},\"dependencies\":{\"clap\":\"4.0\",\"serde\":\"1.0\",\"tokio\":{\"version\":\"1.0\",\"features\":[\"full\"]}},\"features\":{\"default\":[\"json\"],\"json\":[\"serde_json\"],\"yaml\":[\"serde_yaml\"]},\"dev-dependencies\":{\"assert_cmd\":\"2.0\",\"predicates\":\"3.0\"},\"bin\":[{\"name\":\"test-cli\",\"path\":\"src/main.rs\"}],\"build-dependencies\":{\"cc\":\"1.0\"},\"profile\":{\"release\":{\"codegen-units\":1,\"lto\":true,\"opt-level\":3}},\"workspace\":{\"members\":[\"crate1\",\"crate2\",\"subdir/crate3\"]},\"metadata\":{\"docs\":{\"rs\":{\"all-features\":true,\"rustdoc-args\":[\"--cfg\",\"docsrs\"]}}},\"config\":{\"debug\":true,\"max_connections\":100,\"timeout\":30,\"database\":{\"name\":\"testdb\",\"host\":\"localhost\",\"password\":\"secret\",\"port\":5432,\"user\":\"admin\"},\"server\":{\"host\":\"0.0.0.0\",\"port\":8080,\"workers\":4}},\"database\":{\"connection\":[{\"server\":\"192.168.1.1\",\"enabled\":true,\"connection_max\":5000,\"ports\":[8001,8001,8002]},{\"server\":\"192.168.1.2\",\"enabled\":false,\"connection_max\":300,\"ports\":[8001]}]},\"servers\":[{\"name\":\"alpha\",\"ip\":\"10.0.0.1\",\"role\":\"frontend\"},{\"name\":\"beta\",\"ip\":\"10.0.0.2\",\"role\":\"backend\"}],\"logging\":{\"level\":\"info\",\"format\":\"json\",\"file\":{\"path\":\"/var/log/app.log\",\"max_size\":\"10MB\",\"rotate\":true},\"console\":{\"colors\":true,\"enabled\":true,\"test_booleans\":[true,false,true],\"test_floats\":[3.14,2.71,1.41],\"test_numbers\":[1,2,3,42],\"test_strings\":[\"hello\",\"world\",\"test\"]}},\"mixed_data\":{\"array_value\":[1,2,3],\"boolean_value\":true,\"date_value\":\"2024-01-01T00:00:00Z\",\"float_value\":3.14159,\"integer_value\":42,\"string_value\":\"test string\",\"nested\":{\"key1\":\"value1\",\"key2\":\"value2\",\"bin_number\":0,\"b11010110\":null,\"float_with_exponent\":5,\"e\":22,\"float_with_underscore\":224617.445991,\"hex_number\":0,\"xDEADBEEF\":null,\"int_with_underscore\":1000000,\"oct_number\":0,\"o755\":null,\"local_date\":\"1979-05-27\",\"local_datetime\":\"1979-05-27T07:32:00\",\"local_time\":\"07:32:00\",\"offset_datetime\":\"1979-05-27T07:32:00-08:00\",\"heterogeneous_array\":[1,2,\"a\",\"b\",\"c\"],\"nested_array\":[\"gamma\",\"delta\",1,2],\"inline_table\":{\"x\":1,\"y\":2},\"nested_inline\":{\"person\":{\"name\":\"John\",\"age\":30}},\"infinity\":9223372036854775807,\"negative_infinity\":-9223372036854775808,\"not_a_number\":NaN,\"empty_array\":[],\"empty_string\":\"\",\"\":null,\"russian_comment\":\"Привет мир\"}},\"products\":[{\"name\":\"Hammer\",\"sku\":738594937},{\"name\":\"Nail\",\"sku\":284758393,\"color\":\"gray\"}],\"tool\":{\"name\":\"cargo\",\"version\":\"1.70.0\",\"features\":{\"default\":[\"std\"],\"no-std\":[\"core\"],\"std\":[]},\"settings\":{\"format\":\"json\",\"compression\":true}},\"127.0.0.1\":\"localhost\",\"barke_key\":\"value\",\"character encoding\":\"UTF-8\",\"quoted_key\":\"value\",\"ʎǝʞ\":\"upside down key\"}") end | let yaml_input = "---\nstring: hello\nnumber: 42\nfloat: 3.14\nbool_true: true\nbool_false: false\nnull_value: null\narray:\n - item1\n - item2\n - item3\nobject:\n key1: value1\n key2: value2\nnested:\n arr:\n - a\n - b\n obj:\n subkey: subval\nmultiline: |\n This is a\n multiline string\nquoted: \"quoted string\"\nsingle_quoted: 'single quoted string'\ndate: 2024-06-01\ntimestamp: 2024-06-01T12:34:56Z\nempty_array: []\nempty_object: {}\nanchors:\n &anchor_val anchored value\nref: *anchor_val\ncomplex:\n - foo: bar\n baz:\n - qux\n - quux\n - corge: grault\nspecial_chars: \"!@#$%^&*()_+-=[]{}|;:',.<>/?\"\nunicode: \"こんにちは世界\"\nbool_list:\n - true\n - false\nnull_list:\n - null\n - ~" diff --git a/crates/mq-lang/src/ast/parser.rs b/crates/mq-lang/src/ast/parser.rs index 7455f70dc..42a651b05 100644 --- a/crates/mq-lang/src/ast/parser.rs +++ b/crates/mq-lang/src/ast/parser.rs @@ -1094,7 +1094,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { return Err(SyntaxError::UnexpectedToken((**while_token).clone())); } - self.consume_colon(); + // Check for 'do' keyword + if self.is_next_token(|kind| matches!(kind, TokenKind::Do)) { + let _ = self.next_token(|kind| matches!(kind, TokenKind::Do)); + } else { + self.consume_colon(); + } match self.tokens.peek() { Some(_) => { @@ -1218,7 +1223,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { name: ident, token: ident_token, }) => { - self.consume_colon(); + // Check for 'do' keyword + if self.is_next_token(|kind| matches!(kind, TokenKind::Do)) { + let _ = self.next_token(|kind| matches!(kind, TokenKind::Do)); + } else { + self.consume_colon(); + } let each_values = Shared::clone(&args[1]); let body_program = self.parse_program(false)?; @@ -1285,7 +1295,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } let value = Shared::clone(args.first().unwrap()); - self.consume_colon(); + // Check for 'do' keyword + if self.is_next_token(|kind| matches!(kind, TokenKind::Do)) { + let _ = self.next_token(|kind| matches!(kind, TokenKind::Do)); + } else { + self.consume_colon(); + } // Parse match arms let mut arms: super::node::MatchArms = SmallVec::new(); @@ -2903,6 +2918,29 @@ mod tests { token(TokenKind::Colon), ], Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::While, module_id: 1.into()})))] + #[case::while_do_end( + vec![ + token(TokenKind::While), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Do), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::End), + ], + Ok(vec![Shared::new(Node { + token_id: 0.into(), + expr: Shared::new(Expr::While( + Shared::new(Node { + token_id: 1.into(), + expr: Shared::new(Expr::Literal(Literal::Bool(true))), + }), + vec![Shared::new(Node { + token_id: 3.into(), + expr: Shared::new(Expr::Literal(Literal::String("loop body".to_owned()))), + })], + )), + })]))] #[case::loop_( vec![ token(TokenKind::Loop), @@ -3007,6 +3045,52 @@ mod tests { token(TokenKind::SemiColon), ], Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Foreach, module_id: 1.into()})))] + #[case::foreach_do_end( + vec![ + token(TokenKind::Foreach), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::Comma), + token(TokenKind::StringLiteral("array".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Do), + token(TokenKind::Ident(SmolStr::new("print"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::RParen), + token(TokenKind::End), + ], + Ok(vec![Shared::new(Node { + token_id: 6.into(), + expr: Shared::new(Expr::Foreach( + IdentWithToken::new_with_token( + "item", + Some(Shared::new(token(TokenKind::Ident(SmolStr::new("item"))))), + ), + Shared::new(Node { + token_id: 1.into(), + expr: Shared::new(Expr::Literal(Literal::String("array".to_owned()))), + }), + vec![Shared::new(Node { + token_id: 4.into(), + expr: Shared::new(Expr::Call( + IdentWithToken::new_with_token( + "print", + Some(Shared::new(token(TokenKind::Ident(SmolStr::new( + "print", + ))))), + ), + smallvec![Shared::new(Node { + token_id: 3.into(), + expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + "item", + Some(Shared::new(token(TokenKind::Ident(SmolStr::new("item"))))), + ))), + })], + )), + })], + )), + })]))] #[case::self_( vec![token(TokenKind::Self_), token(TokenKind::Eof)], Ok(vec![Shared::new(Node { @@ -6826,6 +6910,65 @@ mod tests { )) }) ]))] + #[case::match_do_end( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RParen), + token(TokenKind::Do), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("one".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("two".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("other".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Shared::new(Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Shared::new(Expr::Literal(Literal::Number(2.into()))) + }), + smallvec![ + MatchArm { + pattern: Pattern::Literal(Literal::Number(1.into())), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Shared::new(Expr::Literal(Literal::String("one".to_owned()))) + }) + }, + MatchArm { + pattern: Pattern::Literal(Literal::Number(2.into())), + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Shared::new(Expr::Literal(Literal::String("two".to_owned()))) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 11.into(), + expr: Shared::new(Expr::Literal(Literal::String("other".to_owned()))) + }) + } + ] + )) + }) + ]))] fn test_parse_match(#[case] input: Vec, #[case] expected: Result) { let mut arena = Arena::new(10); let tokens: Vec> = input.into_iter().map(Shared::new).collect(); diff --git a/crates/mq-lang/src/cst/parser.rs b/crates/mq-lang/src/cst/parser.rs index 6505d4b47..08ed1ca0d 100644 --- a/crates/mq-lang/src/cst/parser.rs +++ b/crates/mq-lang/src/cst/parser.rs @@ -1478,7 +1478,12 @@ impl<'a> Parser<'a> { children.push(self.parse_expr(leading_trivia, false, false)?); children.push(self.next_node(|kind| matches!(kind, TokenKind::RParen), NodeKind::Token)?); - self.push_colon_token_if_present(&mut children)?; + // Check for 'do' keyword + if self.try_next_token(|kind| matches!(kind, TokenKind::Do)) { + children.push(self.next_node(|kind| matches!(kind, TokenKind::Do), NodeKind::Token)?); + } else { + self.push_colon_token_if_present(&mut children)?; + } let (mut program, _) = self.parse_program(false, true); @@ -1508,7 +1513,12 @@ impl<'a> Parser<'a> { children.push(self.parse_expr(leading_trivia, false, true)?); children.push(self.next_node(|kind| matches!(kind, TokenKind::RParen), NodeKind::Token)?); - self.push_colon_token_if_present(&mut children)?; + // Check for 'do' keyword + if self.try_next_token(|kind| matches!(kind, TokenKind::Do)) { + children.push(self.next_node(|kind| matches!(kind, TokenKind::Do), NodeKind::Token)?); + } else { + self.push_colon_token_if_present(&mut children)?; + } let (mut program, _) = self.parse_program(false, true); @@ -1614,7 +1624,12 @@ impl<'a> Parser<'a> { } children.append(&mut args); - self.push_colon_token_if_present(&mut children)?; + // Check for 'do' keyword + if self.try_next_token(|kind| matches!(kind, TokenKind::Do)) { + children.push(self.next_node(|kind| matches!(kind, TokenKind::Do), NodeKind::Token)?); + } else { + self.push_colon_token_if_present(&mut children)?; + } // Parse match arms loop { @@ -3095,6 +3110,91 @@ mod tests { ErrorReporter::default() ) )] + #[case::foreach_do_end( + vec![ + Shared::new(token(TokenKind::Foreach)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("item".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("collection".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Foreach, + token: Some(Shared::new(token(TokenKind::Foreach))), + leading_trivia: Vec::new(), + trailing_trivia: vec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + children: vec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident, + token: Some(Shared::new(token(TokenKind::Ident("item".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident, + token: Some(Shared::new(token(TokenKind::Ident("collection".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: vec![Trivia::NewLine], + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: vec![Trivia::NewLine], + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + ], + }), + ], + ErrorReporter::default() + ) + )] #[case::foreach_value_is_function_call( vec![ Shared::new(token(TokenKind::Foreach)), @@ -3259,6 +3359,75 @@ mod tests { ErrorReporter::default() ) )] + #[case::while_do_end( + vec![ + Shared::new(token(TokenKind::While)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::While, + token: Some(Shared::new(token(TokenKind::While))), + leading_trivia: Vec::new(), + trailing_trivia: vec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + children: vec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident, + token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: vec![Trivia::NewLine], + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: vec![Trivia::NewLine], + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + ], + }), + ], + ErrorReporter::default() + ) + )] #[case::loop_( vec![ Shared::new(token(TokenKind::Loop)), @@ -6348,6 +6517,155 @@ mod tests { ErrorReporter::default() ) )] + #[case::match_do_end( + vec![ + Shared::new(token(TokenKind::Match)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("one".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("_".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("other".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Match, + token: Some(Shared::new(token(TokenKind::Match))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: vec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::MatchArm, + token: None, + leading_trivia: vec![Trivia::NewLine], + trailing_trivia: Vec::new(), + children: vec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Pattern, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("one".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + ], + }), + Shared::new(Node { + kind: NodeKind::MatchArm, + token: None, + leading_trivia: vec![Trivia::NewLine], + trailing_trivia: Vec::new(), + children: vec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Pattern, + token: Some(Shared::new(token(TokenKind::Ident("_".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("other".into())))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + ], + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + ], + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: Vec::new(), + trailing_trivia: Vec::new(), + children: Vec::new(), + }), + ], + ErrorReporter::default() + ) + )] #[case::index_access_with_function_call( vec![ Shared::new(token(TokenKind::Ident("arr".into()))), diff --git a/crates/mq-lang/tests/integration_tests.rs b/crates/mq-lang/tests/integration_tests.rs index b506c5e64..ba9f65f6a 100644 --- a/crates/mq-lang/tests/integration_tests.rs +++ b/crates/mq-lang/tests/integration_tests.rs @@ -73,6 +73,73 @@ fn engine() -> DefaultEngine { ", vec![RuntimeValue::Number(0.into())], Ok(vec![RuntimeValue::Array(vec![RuntimeValue::Number(11.into()), RuntimeValue::Number(12.into()), RuntimeValue::Number(14.into()), RuntimeValue::Number(15.into())])].into()))] +#[case::while_do_end(" + let x = 5 | + while (x > 0) do + let x = x - 1 | x + end + ", + vec![RuntimeValue::Number(10.into())], + Ok(vec![RuntimeValue::Number(0.into())].into()))] +#[case::foreach_do_end(" + foreach(x, array(1, 2, 3)) do + add(x, 1) + end + ", + vec![RuntimeValue::Number(10.into())], + Ok(vec![RuntimeValue::Array(vec![RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into()), RuntimeValue::Number(4.into())])].into()))] +#[case::while_do_end_break(" + let x = 0 | + while(x < 10) do + let x = x + 1 + | if(x == 3): + break + else: + x + end + end + ", + vec![RuntimeValue::Number(10.into())], + Ok(vec![RuntimeValue::Number(2.into())].into()))] +#[case::foreach_do_end_continue(" + foreach(x, array(1, 2, 3, 4, 5)) do + if(x == 3): + continue + else: + x + 10 + end + end + ", + vec![RuntimeValue::Number(0.into())], + Ok(vec![RuntimeValue::Array(vec![RuntimeValue::Number(11.into()), RuntimeValue::Number(12.into()), RuntimeValue::Number(14.into()), RuntimeValue::Number(15.into())])].into()))] +#[case::nested_do_end(" + let arr = array(array(1, 2), array(3, 4)) | + foreach(row, arr) do + foreach(x, row) do + x * 2 + end + end + ", + vec![RuntimeValue::Number(0.into())], + Ok(vec![RuntimeValue::Array(vec![RuntimeValue::Array(vec![RuntimeValue::Number(2.into()), RuntimeValue::Number(4.into())]), RuntimeValue::Array(vec![RuntimeValue::Number(6.into()), RuntimeValue::Number(8.into())])])].into()))] +#[case::match_do_end(" + match (2) do + | 1: \"one\" + | 2: \"two\" + | _: \"other\" + end + ", + vec![RuntimeValue::Number(0.into())], + Ok(vec![RuntimeValue::String("two".to_string())].into()))] +#[case::match_do_end_type_pattern(" + match (array(1, 2, 3)) do + | :array: \"is_array\" + | :number: \"is_number\" + | _: \"other\" + end + ", + vec![RuntimeValue::Number(0.into())], + Ok(vec![RuntimeValue::String("is_array".to_string())].into()))] #[case::if_(" def fibonacci(x): if(eq(x, 0)):