From bd8dd8062bbdaebe5f7e1df8945c9424ab096965 Mon Sep 17 00:00:00 2001 From: harehare Date: Thu, 1 Jan 2026 22:50:25 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20feat(lang):=20add=20do...end=20?= =?UTF-8?q?block=20syntax=20for=20while,=20foreach,=20and=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for do...end block syntax as an alternative to colon syntax for while, foreach, and match constructs. Both syntaxes are now supported simultaneously for backward compatibility. Changes: - Modified CST parser to accept 'do' keyword before block bodies - Modified AST parser to handle do...end syntax - Added comprehensive integration tests for new syntax - All existing tests pass, ensuring backward compatibility Examples: while (x > 0) do x - 1 end foreach(item, arr) do process(item) end match (value) do | 1: "one" | 2: "two" end --- crates/mq-lang/src/ast/parser.rs | 21 ++++++- crates/mq-lang/src/cst/parser.rs | 21 ++++++- crates/mq-lang/tests/integration_tests.rs | 67 +++++++++++++++++++++++ 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/crates/mq-lang/src/ast/parser.rs b/crates/mq-lang/src/ast/parser.rs index 7455f70dc..f1712691b 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(); diff --git a/crates/mq-lang/src/cst/parser.rs b/crates/mq-lang/src/cst/parser.rs index 6505d4b47..6234cfbe3 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 { 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)): From 2a054e3754e0da97c13c86baef99205a7c1537d1 Mon Sep 17 00:00:00 2001 From: harehare Date: Thu, 1 Jan 2026 23:25:45 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/mq-lang/src/ast/parser.rs | 128 +++++++++++++ crates/mq-lang/src/cst/parser.rs | 303 +++++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+) diff --git a/crates/mq-lang/src/ast/parser.rs b/crates/mq-lang/src/ast/parser.rs index f1712691b..42a651b05 100644 --- a/crates/mq-lang/src/ast/parser.rs +++ b/crates/mq-lang/src/ast/parser.rs @@ -2918,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), @@ -3022,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 { @@ -6841,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 6234cfbe3..08ed1ca0d 100644 --- a/crates/mq-lang/src/cst/parser.rs +++ b/crates/mq-lang/src/cst/parser.rs @@ -3110,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)), @@ -3274,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)), @@ -6363,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()))), From 169ce9488a2c861cdbbdfdbc45d395826d0b0fcd Mon Sep 17 00:00:00 2001 From: harehare Date: Fri, 2 Jan 2026 08:22:17 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20feat(formatter):=20add=20suppor?= =?UTF-8?q?t=20for=20do...end=20block=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add formatting support for do...end syntax in while, foreach, and match statements. Includes proper spacing around the do keyword and comprehensive tests for various scenarios (oneline, multiline, nested, with guards). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- crates/mq-formatter/src/formatter.rs | 177 +++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 13 deletions(-) diff --git a/crates/mq-formatter/src/formatter.rs b/crates/mq-formatter/src/formatter.rs index c63dc422a..c764051d5 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,25 @@ 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 space before and after + self.append_space(); + self.format_node(mq_lang::Shared::clone(separator_node), 0); + + // Add space after 'do' if next node doesn't have newline + if let Some(next) = expr_nodes.peek() + && !next.has_new_line() + { + self.append_space(); + } + } 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 +826,36 @@ 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 + if let Some(separator) = node.children.get(separator_pos) { + if uses_do_syntax { + // Format 'do' keyword with space before + self.append_space(); + self.format_node(mq_lang::Shared::clone(separator), 0); + + // Add space after 'do' if next node doesn't have newline and is not a MatchArm + // (MatchArm starts with pipe which handles its own spacing) + if let Some(next) = node.children.get(separator_pos + 1) + && !next.has_new_line() + && !matches!(next.kind, mq_lang::CstNodeKind::MatchArm) + { + self.append_space(); + } + } else { + // Format colon + self.format_node(mq_lang::Shared::clone(separator), 0); + } } // Calculate indent level for match arms (similar to format_if) @@ -827,7 +869,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<_> = node.children.iter().skip(separator_pos + 1).collect(); // Check if this is a multiline match (first match arm has new line) let is_multiline = remaining_children @@ -2292,6 +2334,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) { From 68bc447de8c1d41311d0c3dc044dc39a72eb1f99 Mon Sep 17 00:00:00 2001 From: harehare Date: Fri, 2 Jan 2026 10:39:08 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(formatter):?= =?UTF-8?q?=20extract=20do=20keyword=20formatting=20into=20helper=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates duplicate code for formatting 'do' keyword spacing into a new `format_do_with_spacing` helper method, improving maintainability and reducing code duplication. --- crates/mq-formatter/src/formatter.rs | 57 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/mq-formatter/src/formatter.rs b/crates/mq-formatter/src/formatter.rs index c764051d5..e8631eef7 100644 --- a/crates/mq-formatter/src/formatter.rs +++ b/crates/mq-formatter/src/formatter.rs @@ -408,16 +408,8 @@ impl Formatter { && let Some(separator_node) = expr_nodes.next() { if uses_do_syntax { - // Format 'do' keyword with space before and after - self.append_space(); - self.format_node(mq_lang::Shared::clone(separator_node), 0); - - // Add space after 'do' if next node doesn't have newline - if let Some(next) = expr_nodes.peek() - && !next.has_new_line() - { - self.append_space(); - } + // 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); @@ -838,20 +830,12 @@ impl Formatter { } // Format colon or do keyword - if let Some(separator) = node.children.get(separator_pos) { - if uses_do_syntax { - // Format 'do' keyword with space before - self.append_space(); - self.format_node(mq_lang::Shared::clone(separator), 0); + let mut remaining_children = node.children.iter().skip(separator_pos).peekable(); - // Add space after 'do' if next node doesn't have newline and is not a MatchArm - // (MatchArm starts with pipe which handles its own spacing) - if let Some(next) = node.children.get(separator_pos + 1) - && !next.has_new_line() - && !matches!(next.kind, mq_lang::CstNodeKind::MatchArm) - { - self.append_space(); - } + 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); @@ -869,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(separator_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 @@ -1387,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); From 64884a7fdd4e8fd2ef4fba72e3df9424ae30fed8 Mon Sep 17 00:00:00 2001 From: harehare Date: Fri, 2 Jan 2026 21:31:14 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=A8=20feat(macro):=20handle=20zero=20?= =?UTF-8?q?iterations=20in=20times=20macro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/mq-lang/builtin.mq | 5 ++++- crates/mq-lang/modules/module_tests.mq | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 - ~"