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..3f517be 100644 --- a/genco-macros/src/encoder.rs +++ b/genco-macros/src/encoder.rs @@ -116,6 +116,9 @@ impl<'a> Encoder<'a> { } => { self.encode_match(condition, arms); } + Ast::Let { name, expr } => { + self.encode_let(name, expr); + } } Ok(()) @@ -303,6 +306,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..f4d7723 100644 --- a/genco-macros/src/quote.rs +++ b/genco-macros/src/quote.rs @@ -244,6 +244,20 @@ 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 +314,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..b6e0fd2 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.\nC 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\nSecond 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..91b9bc1 100644 --- a/tests/test_token_gen.rs +++ b/tests/test_token_gen.rs @@ -322,6 +322,91 @@ 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 x = "bar"; + fn baz(s: &str) -> String { + format!("{s}baz") + } + + let tokens: rust::Tokens = quote! { + $(let a = baz(x)) $a + }; + + assert_eq! { + tokens, + vec![Space, Literal("barbaz".into())] + }; + + // Complex expression + let x = 2; + let tokens: rust::Tokens = quote! { + $(let even = if x % 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