From fa1938fb0d531e4b6862db0881d230a2fe9521ce Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Sat, 18 May 2024 09:18:57 +0200 Subject: [PATCH 1/3] Add support for variables --- genco-macros/src/ast.rs | 6 +++ genco-macros/src/encoder.rs | 14 +++++++ genco-macros/src/quote.rs | 21 +++++++++++ src/lib.rs | 42 +++++++++++++++++++++ tests/test_token_gen.rs | 75 +++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) diff --git a/genco-macros/src/ast.rs b/genco-macros/src/ast.rs index 36d9ad3..c6b1a09 100644 --- a/genco-macros/src/ast.rs +++ b/genco-macros/src/ast.rs @@ -182,6 +182,12 @@ pub(crate) enum Ast { /// Else branch of the conditional. else_branch: Option, }, + Let { + /// Variable name (or names for a tuple) + name: syn::Pat, + /// Expression + expr: syn::Expr, + }, Match { condition: syn::Expr, arms: Vec, diff --git a/genco-macros/src/encoder.rs b/genco-macros/src/encoder.rs index 94c8d6e..1d78b72 100644 --- a/genco-macros/src/encoder.rs +++ b/genco-macros/src/encoder.rs @@ -115,6 +115,11 @@ impl<'a> Encoder<'a> { condition, arms, .. } => { self.encode_match(condition, arms); + }, + Ast::Let { + name, expr + } => { + self.encode_let(name, expr); } } @@ -303,6 +308,15 @@ impl<'a> Encoder<'a> { self.output.extend(m); } + /// Encode a let statement + pub(crate) fn encode_let(&mut self, name: syn::Pat, expr: syn::Expr) { + self.item_buffer.flush(&mut self.output); + + self.output.extend(q::quote! { + let #name = #expr; + }) + } + fn from(&mut self) -> Option { // So we've (potentially) encountered the first ever token, while we // have a spanned start like `quote_in! { out => foo }`, `foo` is now diff --git a/genco-macros/src/quote.rs b/genco-macros/src/quote.rs index b66fba3..f940466 100644 --- a/genco-macros/src/quote.rs +++ b/genco-macros/src/quote.rs @@ -244,6 +244,23 @@ impl<'a> Quote<'a> { Ok((req, Ast::Match { condition, arms })) } + fn parse_let(&self, input: ParseStream) -> Result<(Requirements, Ast)> { + input.parse::()?; + + let req = Requirements::default(); + + let name = syn::Pat::parse_single(input)?; + input.parse::()?; + let expr = syn::Expr::parse_without_eager_brace(input)?; + + let ast = Ast::Let { + name, + expr, + }; + + Ok((req, ast)) + } + /// Parse evaluation: `[*] => `. fn parse_scope(&self, input: ParseStream) -> Result { input.parse::()?; @@ -300,6 +317,10 @@ impl<'a> Quote<'a> { let (req, ast) = self.parse_match(&scope)?; encoder.requirements.merge_with(req); ast + } else if scope.peek(Token![let]) { + let (req, ast) = self.parse_let(&scope)?; + encoder.requirements.merge_with(req); + ast } else if scope.peek(Token![ref]) { self.parse_scope(&scope)? } else if crate::string_parser::is_lit_str_opt(scope.fork())? { diff --git a/src/lib.rs b/src/lib.rs index 15e49fb..df2bd44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -629,6 +629,48 @@ /// ///
/// +/// # Variable assignment +/// +/// You can use `$(let = )` to define variables with their value. +/// This is useful within loops to compute values from iterator items. +/// +/// ``` +/// use genco::prelude::*; +/// +/// let names = ["A.B", "C.D"]; +/// +/// let tokens: Tokens<()> = quote! { +/// $(for name in names => +/// $(let (first, second) = name.split_once('.').unwrap()) +/// $first and $second. +/// ) +/// }; +/// assert_eq!(" A and B. C and D.", tokens.to_string()?); +/// # Ok::<_, genco::fmt::Error>(()) +/// ``` +/// +/// Variables can also be mutable: +/// +/// ``` +/// use genco::prelude::*; +/// let path = "A.B.C.D"; +/// +/// let tokens: Tokens<()> = quote! { +/// $(let mut items = path.split('.')) +/// $(if let Some(first) = items.next() => +/// First is $first +/// ) +/// $(if let Some(second) = items.next() => +/// Second is $second +/// ) +/// }; +/// +/// assert_eq!(" First is A Second is B", tokens.to_string()?); +/// # Ok::<_, genco::fmt::Error>(()) +/// ``` +/// +///
+/// /// # Scopes /// /// You can use `$(ref { })` to gain access to the current diff --git a/tests/test_token_gen.rs b/tests/test_token_gen.rs index 6923aab..1a68b69 100644 --- a/tests/test_token_gen.rs +++ b/tests/test_token_gen.rs @@ -322,6 +322,81 @@ fn test_match() { }; } +#[test] +fn test_let() { + let tokens: rust::Tokens = quote! { + $(let x = 1) $x + }; + + assert_eq! { + tokens, + vec![Space, Literal("1".into())] + }; + + // Tuple binding + let tokens: rust::Tokens = quote! { + $(let (a, b) = ("c", "d")) $a, $b + }; + + assert_eq! { + tokens, + vec![ + Space, Literal("c".into()), + Literal(Static(",")), + Space, Literal("d".into()) + ] + }; + + // Function call in expression + let foo = "bar"; + fn baz(s: &str) -> String { + format!("{s}baz") + } + + let tokens: rust::Tokens = quote! { + $(let a = baz(foo)) $a + }; + + assert_eq! { + tokens, + vec![Space, Literal("barbaz".into())] + }; + + // Complex expression + let foo = 2; + let tokens: rust::Tokens = quote! { + $(let even = if foo % 2 == 0 { "even" } else { "odd" }) $even + }; + + assert_eq! { + tokens, + vec![Space, Literal("even".into())] + }; +} + +#[test] +fn test_mutable_let() { + let path = "A.B.C.D"; + + let tokens: Tokens<()> = quote! { + $(let mut items = path.split('.')) + $(if let Some(first) = items.next() => + First is $first + ) + $(if let Some(second) = items.next() => + Second is $second + ) + }; + + assert_eq!( + tokens, + vec![ + Push, Literal(Static("First")), Space, Literal(Static("is")), Space, Literal("A".into()), + Push, Literal(Static("Second")), Space, Literal(Static("is")), Space, Literal("B".into()) + ] + ); +} + #[test] fn test_empty_loop_whitespace() { // Bug: This should generate two commas. But did generate a space following From 0f6d03c608756b66dc458d6330ae9a19a72e10be Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Sat, 18 May 2024 15:02:17 +0200 Subject: [PATCH 2/3] Fix doctests --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index df2bd44..b6e0fd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -645,7 +645,7 @@ /// $first and $second. /// ) /// }; -/// assert_eq!(" A and B. C and D.", tokens.to_string()?); +/// assert_eq!("A and B.\nC and D.", tokens.to_string()?); /// # Ok::<_, genco::fmt::Error>(()) /// ``` /// @@ -665,7 +665,7 @@ /// ) /// }; /// -/// assert_eq!(" First is A Second is B", tokens.to_string()?); +/// assert_eq!("First is A\nSecond is B", tokens.to_string()?); /// # Ok::<_, genco::fmt::Error>(()) /// ``` /// From 56c604e4879eb72e87b6b3e9b2de7df4c2089691 Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Sat, 18 May 2024 15:03:05 +0200 Subject: [PATCH 3/3] Fix clippy and format issues --- genco-macros/src/encoder.rs | 6 ++---- genco-macros/src/quote.rs | 5 +---- tests/test_token_gen.rs | 22 ++++++++++++++++------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/genco-macros/src/encoder.rs b/genco-macros/src/encoder.rs index 1d78b72..3f517be 100644 --- a/genco-macros/src/encoder.rs +++ b/genco-macros/src/encoder.rs @@ -115,10 +115,8 @@ impl<'a> Encoder<'a> { condition, arms, .. } => { self.encode_match(condition, arms); - }, - Ast::Let { - name, expr - } => { + } + Ast::Let { name, expr } => { self.encode_let(name, expr); } } diff --git a/genco-macros/src/quote.rs b/genco-macros/src/quote.rs index f940466..f4d7723 100644 --- a/genco-macros/src/quote.rs +++ b/genco-macros/src/quote.rs @@ -253,10 +253,7 @@ impl<'a> Quote<'a> { input.parse::()?; let expr = syn::Expr::parse_without_eager_brace(input)?; - let ast = Ast::Let { - name, - expr, - }; + let ast = Ast::Let { name, expr }; Ok((req, ast)) } diff --git a/tests/test_token_gen.rs b/tests/test_token_gen.rs index 1a68b69..91b9bc1 100644 --- a/tests/test_token_gen.rs +++ b/tests/test_token_gen.rs @@ -348,13 +348,13 @@ fn test_let() { }; // Function call in expression - let foo = "bar"; + let x = "bar"; fn baz(s: &str) -> String { format!("{s}baz") } let tokens: rust::Tokens = quote! { - $(let a = baz(foo)) $a + $(let a = baz(x)) $a }; assert_eq! { @@ -363,9 +363,9 @@ fn test_let() { }; // Complex expression - let foo = 2; + let x = 2; let tokens: rust::Tokens = quote! { - $(let even = if foo % 2 == 0 { "even" } else { "odd" }) $even + $(let even = if x % 2 == 0 { "even" } else { "odd" }) $even }; assert_eq! { @@ -391,8 +391,18 @@ fn test_mutable_let() { assert_eq!( tokens, vec![ - Push, Literal(Static("First")), Space, Literal(Static("is")), Space, Literal("A".into()), - Push, Literal(Static("Second")), Space, Literal(Static("is")), Space, Literal("B".into()) + Push, + Literal(Static("First")), + Space, + Literal(Static("is")), + Space, + Literal("A".into()), + Push, + Literal(Static("Second")), + Space, + Literal(Static("is")), + Space, + Literal("B".into()) ] ); }