From af27a50cca3835d914a3dcf8a11927b7ece02c83 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 7 Jan 2026 16:22:20 +0800 Subject: [PATCH 1/3] simplify single quote usage; tag 0.2.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- examples/test_escape_debug.rs | 130 +++++++++++++++++----------------- src/primes.rs | 1 - src/writer.rs | 1 - tests/parser_test.rs | 10 +-- tests/writer_cirru/cond.cirru | 2 +- tests/writer_test.rs | 22 ++++++ 8 files changed, 92 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b56f60..a29759e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ [[package]] name = "cirru_parser" -version = "0.2.0" +version = "0.2.1" dependencies = [ "bincode", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 2a9976b..209ba7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cirru_parser" -version = "0.2.0" +version = "0.2.1" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/examples/test_escape_debug.rs b/examples/test_escape_debug.rs index 9487933..4920c7d 100644 --- a/examples/test_escape_debug.rs +++ b/examples/test_escape_debug.rs @@ -3,77 +3,77 @@ use cirru_parser::{parse, print_error}; fn main() { - println!("πŸ” Testing escape_debug in error messages\n"); - println!("{}\n", "═".repeat(70)); + println!("πŸ” Testing escape_debug in error messages\n"); + println!("{}\n", "═".repeat(70)); - // Test 1: Newline in string - println!("Test 1: Newline character in string"); - let code1 = "defn test\n print \"hello\nworld\""; - println!("Code: {code1:?}\n"); - if let Err(e) = parse(code1) { - print_error(&e, Some(code1)); - } - println!("\n{}\n", "─".repeat(70)); + // Test 1: Newline in string + println!("Test 1: Newline character in string"); + let code1 = "defn test\n print \"hello\nworld\""; + println!("Code: {code1:?}\n"); + if let Err(e) = parse(code1) { + print_error(&e, Some(code1)); + } + println!("\n{}\n", "─".repeat(70)); - // Test 2: Tab indentation error - println!("Test 2: Tab character causing indentation issue"); - let code2 = "defn test\n\tabc"; // Tab character - println!("Code: {code2:?}\n"); - if let Err(e) = parse(code2) { - print_error(&e, Some(code2)); - } - println!("\n{}\n", "─".repeat(70)); + // Test 2: Tab indentation error + println!("Test 2: Tab character causing indentation issue"); + let code2 = "defn test\n\tabc"; // Tab character + println!("Code: {code2:?}\n"); + if let Err(e) = parse(code2) { + print_error(&e, Some(code2)); + } + println!("\n{}\n", "─".repeat(70)); - // Test 3: Multiple spaces (odd indentation) - println!("Test 3: Odd number of spaces (visible with escape_debug)"); - let code3 = "defn test\n abc\n def"; // 3 spaces - println!("Code: {code3:?}\n"); - if let Err(e) = parse(code3) { - print_error(&e, Some(code3)); - } - println!("\n{}\n", "─".repeat(70)); + // Test 3: Multiple spaces (odd indentation) + println!("Test 3: Odd number of spaces (visible with escape_debug)"); + let code3 = "defn test\n abc\n def"; // 3 spaces + println!("Code: {code3:?}\n"); + if let Err(e) = parse(code3) { + print_error(&e, Some(code3)); + } + println!("\n{}\n", "─".repeat(70)); - // Test 4: Mixed whitespace - println!("Test 4: Carriage return in string"); - let code4 = "print \"hello\rworld\""; - println!("Code: {code4:?}\n"); - match parse(code4) { - Ok(_) => println!("βœ… Parsed successfully (\\r is valid in identifier)"), - Err(e) => print_error(&e, Some(code4)), - } - println!("\n{}\n", "─".repeat(70)); + // Test 4: Mixed whitespace + println!("Test 4: Carriage return in string"); + let code4 = "print \"hello\rworld\""; + println!("Code: {code4:?}\n"); + match parse(code4) { + Ok(_) => println!("βœ… Parsed successfully (\\r is valid in identifier)"), + Err(e) => print_error(&e, Some(code4)), + } + println!("\n{}\n", "─".repeat(70)); - // Test 5: Long line with special chars - println!("Test 5: Long line with special characters and error"); - let code5 = "defn process-long-text (data)\n let result \"This is a very long string with \\t tabs and \\n newlines \\x error\""; - println!("Code: {code5:?}\n"); - if let Err(e) = parse(code5) { - print_error(&e, Some(code5)); - } - println!("\n{}\n", "─".repeat(70)); + // Test 5: Long line with special chars + println!("Test 5: Long line with special characters and error"); + let code5 = "defn process-long-text (data)\n let result \"This is a very long string with \\t tabs and \\n newlines \\x error\""; + println!("Code: {code5:?}\n"); + if let Err(e) = parse(code5) { + print_error(&e, Some(code5)); + } + println!("\n{}\n", "─".repeat(70)); - // Test 6: Unicode with newline - println!("Test 6: Unicode escape with newline nearby"); - let code6 = "print \"emoji \\u{1F600}\ntext\""; - println!("Code: {code6:?}\n"); - if let Err(e) = parse(code6) { - print_error(&e, Some(code6)); - } - println!("\n{}\n", "─".repeat(70)); + // Test 6: Unicode with newline + println!("Test 6: Unicode escape with newline nearby"); + let code6 = "print \"emoji \\u{1F600}\ntext\""; + println!("Code: {code6:?}\n"); + if let Err(e) = parse(code6) { + print_error(&e, Some(code6)); + } + println!("\n{}\n", "─".repeat(70)); - // Test 7: Indentation with spaces visible - println!("Test 7: Clear space visualization"); - let code7 = "defn main\n line1\n line2\n line3"; // 5 spaces on line3 - println!("Code with explicit spaces: 'defn main\\n line1\\n line2\\n line3'\n"); - if let Err(e) = parse(code7) { - print_error(&e, Some(code7)); - } + // Test 7: Indentation with spaces visible + println!("Test 7: Clear space visualization"); + let code7 = "defn main\n line1\n line2\n line3"; // 5 spaces on line3 + println!("Code with explicit spaces: 'defn main\\n line1\\n line2\\n line3'\n"); + if let Err(e) = parse(code7) { + print_error(&e, Some(code7)); + } - println!("\n{}\n", "═".repeat(70)); - println!("✨ Now you can clearly see:"); - println!(" β€’ \\n for newlines"); - println!(" β€’ \\t for tabs"); - println!(" β€’ Spaces are preserved and visible"); - println!(" β€’ Special escape sequences are shown"); - println!(" β€’ Context window is ~60 chars (increased from 40)"); + println!("\n{}\n", "═".repeat(70)); + println!("✨ Now you can clearly see:"); + println!(" β€’ \\n for newlines"); + println!(" β€’ \\t for tabs"); + println!(" β€’ Spaces are preserved and visible"); + println!(" β€’ Special escape sequences are shown"); + println!(" β€’ Context window is ~60 chars (increased from 40)"); } diff --git a/src/primes.rs b/src/primes.rs index c6103cd..4152c9f 100644 --- a/src/primes.rs +++ b/src/primes.rs @@ -241,7 +241,6 @@ pub fn escape_cirru_leaf(s: &str) -> String { '\t' => chunk.push_str("\\t"), '\"' => chunk.push_str("\\\""), '\\' => chunk.push_str("\\\\"), - '\'' => chunk.push_str("\\'"), _ => chunk.push(c), } } diff --git a/src/writer.rs b/src/writer.rs index f6eef59..09f974b 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -81,7 +81,6 @@ fn generate_leaf(s: &str) -> String { '\t' => ret.push_str("\\t"), '\"' => ret.push_str("\\\""), '\\' => ret.push_str("\\\\"), - '\'' => ret.push_str("\\'"), _ => ret.push(c), } } diff --git a/tests/parser_test.rs b/tests/parser_test.rs index 1745c21..e6c21d9 100644 --- a/tests/parser_test.rs +++ b/tests/parser_test.rs @@ -11,20 +11,14 @@ mod json_test { fn parse_demo() { assert_eq!(parse("a").map(Cirru::List), Ok(Cirru::List(vec!(vec!["a"].into())))); - assert_eq!( - parse("a b c").map(Cirru::List), - Ok(Cirru::List(vec!(vec!["a", "b", "c"].into()))) - ); + assert_eq!(parse("a b c").map(Cirru::List), Ok(Cirru::List(vec!(vec!["a", "b", "c"].into())))); assert_eq!( parse("a\nb").map(Cirru::List), Ok(Cirru::List(vec!(vec!["a"].into(), vec!["b"].into()))) ); - assert_eq!( - parse("a\rb").map(Cirru::List), - Ok(Cirru::List(vec!(vec!["a\rb"].into()))) - ); + assert_eq!(parse("a\rb").map(Cirru::List), Ok(Cirru::List(vec!(vec!["a\rb"].into())))); assert_eq!( parse("a (b) c").map(Cirru::List), diff --git a/tests/writer_cirru/cond.cirru b/tests/writer_cirru/cond.cirru index b00952c..51b3c8d 100644 --- a/tests/writer_cirru/cond.cirru +++ b/tests/writer_cirru/cond.cirru @@ -5,7 +5,7 @@ cond (bool? x) str x (symbol? x) - str "\"\'" x + str "\"'" x (map? x) "\"a map" (set? x) "\"a set" true $ str diff --git a/tests/writer_test.rs b/tests/writer_test.rs index 158a54a..30af063 100644 --- a/tests/writer_test.rs +++ b/tests/writer_test.rs @@ -123,6 +123,28 @@ fn leaves_escapeing() { assert_eq!("\"δΈ­ζ–‡\\n\"", escape_cirru_leaf("δΈ­ζ–‡\n")); } +#[test] +fn leaf_single_quote_without_quotes() -> Result<(), String> { + use cirru_parser::{Cirru, CirruWriterOptions, format}; + + let xs = vec![Cirru::List(vec![Cirru::leaf("foo'bar")])]; + let rendered = format(&xs, CirruWriterOptions::from(false))?; + + assert_eq!("\nfoo'bar\n", rendered); + Ok(()) +} + +#[test] +fn leaf_single_quote_with_spaces_requires_quotes() -> Result<(), String> { + use cirru_parser::{Cirru, CirruWriterOptions, format}; + + let xs = vec![Cirru::List(vec![Cirru::leaf("foo bar'baz")])]; + let rendered = format(&xs, CirruWriterOptions::from(false))?; + + assert_eq!("\n\"foo bar'baz\"\n", rendered); + Ok(()) +} + #[test] fn test_writer_options_from_bool() -> Result<(), String> { use cirru_parser::{Cirru, CirruWriterOptions, format}; From d59feb88fe7d449d26f0a9f372d95685469e48fb Mon Sep 17 00:00:00 2001 From: tiye Date: Sat, 10 Jan 2026 12:45:36 +0800 Subject: [PATCH 2/3] remove trailing space after dollar sign; tag 0.2.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/writer.rs | 15 ++++++-- tests/writer_cirru/tag-match.cirru | 4 +++ tests/writer_data/tag-match.json | 10 ++++++ tests/writer_test.rs | 57 ++++++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tests/writer_cirru/tag-match.cirru create mode 100644 tests/writer_data/tag-match.json diff --git a/Cargo.lock b/Cargo.lock index a29759e..b9df94a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ [[package]] name = "cirru_parser" -version = "0.2.1" +version = "0.2.2" dependencies = [ "bincode", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 209ba7f..7fbd93f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cirru_parser" -version = "0.2.1" +version = "0.2.2" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/src/writer.rs b/src/writer.rs index 09f974b..b05191c 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -196,9 +196,18 @@ fn generate_tree( if ys.is_empty() { String::from("$") } else { - let mut ret = String::from("$ "); - ret.push_str(&generate_tree(ys, false, options, level, at_tail)?); - ret + let content = generate_tree(ys, false, options, level, at_tail)?; + if content.starts_with('\n') { + // If content starts with newline, don't add space after $ + let mut ret = String::from("$"); + ret.push_str(&content); + ret + } else { + // Otherwise, add space after $ + let mut ret = String::from("$ "); + ret.push_str(&content); + ret + } } } else if idx == 0 && insist_head { generate_inline_expr(ys) diff --git a/tests/writer_cirru/tag-match.cirru b/tests/writer_cirru/tag-match.cirru new file mode 100644 index 0000000..15b5a4c --- /dev/null +++ b/tests/writer_cirru/tag-match.cirru @@ -0,0 +1,4 @@ + +tag-match self $ + :plugin node cursor state + d! cursor $ assoc state :show? false diff --git a/tests/writer_data/tag-match.json b/tests/writer_data/tag-match.json new file mode 100644 index 0000000..9de1bce --- /dev/null +++ b/tests/writer_data/tag-match.json @@ -0,0 +1,10 @@ +[ + [ + "tag-match", + "self", + [ + [":plugin", "node", "cursor", "state"], + ["d!", "cursor", ["assoc", "state", ":show?", "false"]] + ] + ] +] diff --git a/tests/writer_test.rs b/tests/writer_test.rs index 30af063..d0ac8a6 100644 --- a/tests/writer_test.rs +++ b/tests/writer_test.rs @@ -61,6 +61,7 @@ mod json_write_test { "spaces", "unfolding", "list-match", + "tag-match", ]; for file in files { println!("testing file: {file}"); @@ -176,3 +177,59 @@ fn test_writer_options_from_bool() -> Result<(), String> { Ok(()) } + +#[cfg(feature = "serde-json")] +#[test] +fn test_dollar_sign_spacing() -> Result<(), String> { + use cirru_parser::{Cirru, CirruWriterOptions, format, from_json_str}; + + // Test case from user: tag-match with nested structures + let json_str = r#"[ + [ + "tag-match", + "self", + [ + [ + ":plugin", + "node", + "cursor", + "state" + ], + [ + "d!", + "cursor", + [ + "assoc", + "state", + ":show?", + "false" + ] + ] + ] + ] + ]"#; + + let writer_options = CirruWriterOptions { use_inline: false }; + + match from_json_str(json_str) { + Ok(tree) => { + if let Cirru::List(xs) = tree { + let result = format(&xs, writer_options)?; + println!("Formatted result:\n{}", result); + + // Check that there's no "$ \n" pattern (dollar sign followed by space and newline) + assert!(!result.contains("$ \n"), "Found unexpected '$ \\n' pattern in output"); + + // The result should contain "$" followed directly by newline + // when there are nested structures after the dollar sign + Ok(()) + } else { + panic!("unexpected leaf here") + } + } + Err(e) => { + println!("parse error: {e}"); + Err(format!("failed to parse JSON: {e}")) + } + } +} From f63c898b2f4fbfafe1cc0dab0f317893dfc6b28e Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 13 Jan 2026 01:09:31 +0800 Subject: [PATCH 3/3] support dollar in one liner writer; tag 0.2.3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/writer.rs | 16 ++++++++++++- tests/oneliner_test.rs | 54 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9df94a..010092e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ [[package]] name = "cirru_parser" -version = "0.2.2" +version = "0.2.3" dependencies = [ "bincode", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 7fbd93f..9e51035 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cirru_parser" -version = "0.2.2" +version = "0.2.3" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/src/writer.rs b/src/writer.rs index b05191c..d275f9c 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -127,13 +127,27 @@ fn render_newline(n: usize) -> String { fn generate_statement_one_liner(xs: &[Cirru]) -> String { let mut ret = String::new(); + let len = xs.len(); for (idx, cursor) in xs.iter().enumerate() { if idx > 0 { ret.push(' '); } + let at_tail = idx == len - 1 && idx > 0; match cursor { Cirru::Leaf(s) => ret.push_str(&generate_leaf(s)), - Cirru::List(ys) => ret.push_str(&generate_inline_expr(ys)), + Cirru::List(ys) => { + if at_tail { + // Use $ syntax for tail expressions + if ys.is_empty() { + ret.push('$'); + } else { + ret.push_str("$ "); + ret.push_str(&generate_statement_one_liner(ys)); + } + } else { + ret.push_str(&generate_inline_expr(ys)); + } + } } } ret diff --git a/tests/oneliner_test.rs b/tests/oneliner_test.rs index 5d7d287..e60a375 100644 --- a/tests/oneliner_test.rs +++ b/tests/oneliner_test.rs @@ -10,7 +10,8 @@ fn test_format_expr_one_liner() -> Result<(), String> { ]); let one_liner = format_expr_one_liner(&tree)?; - assert_eq!(one_liner, "defn main () (println \"Hello, world!\")"); + // Tail expression should use $ syntax + assert_eq!(one_liner, "defn main () $ println \"Hello, world!\""); let parsed = parse_expr_one_liner(&one_liner)?; assert_eq!(parsed, tree); @@ -38,3 +39,54 @@ fn test_complex_nesting() -> Result<(), String> { Ok(()) } + +#[test] +fn test_tail_expression() -> Result<(), String> { + // Test tail expression with $ syntax + let tree = Cirru::List(vec![ + Cirru::Leaf("defn".into()), + Cirru::Leaf("main".into()), + Cirru::List(vec![]), + Cirru::List(vec![Cirru::Leaf("println".into()), Cirru::Leaf("Hello".into())]), + ]); + + let one_liner = format_expr_one_liner(&tree)?; + assert_eq!(one_liner, "defn main () $ println Hello"); + + let parsed = parse_expr_one_liner(&one_liner)?; + assert_eq!(parsed, tree); + + Ok(()) +} + +#[test] +fn test_nested_tail_expression() -> Result<(), String> { + // Test nested tail expression + let tree = Cirru::List(vec![ + Cirru::Leaf("if".into()), + Cirru::Leaf("condition".into()), + Cirru::List(vec![Cirru::Leaf("do".into()), Cirru::List(vec![Cirru::Leaf("action".into())])]), + ]); + + let one_liner = format_expr_one_liner(&tree)?; + assert_eq!(one_liner, "if condition $ do $ action"); + + let parsed = parse_expr_one_liner(&one_liner)?; + assert_eq!(parsed, tree); + + Ok(()) +} + +#[test] +fn test_empty_tail_expression() -> Result<(), String> { + // Test empty list as tail expression + let tree = Cirru::List(vec![Cirru::Leaf("a".into()), Cirru::Leaf("b".into()), Cirru::List(vec![])]); + + let one_liner = format_expr_one_liner(&tree)?; + assert_eq!(one_liner, "a b $"); + + let parsed = parse_expr_one_liner(&one_liner)?; + assert_eq!(parsed, tree); + + Ok(()) +}