Skip to content

Commit

Permalink
Add support for variable declarations in templates (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
swallez committed May 18, 2024
1 parent 1ec4869 commit db32718
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 0 deletions.
6 changes: 6 additions & 0 deletions genco-macros/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ pub(crate) enum Ast {
/// Else branch of the conditional.
else_branch: Option<TokenStream>,
},
Let {
/// Variable name (or names for a tuple)
name: syn::Pat,
/// Expression
expr: syn::Expr,
},
Match {
condition: syn::Expr,
arms: Vec<MatchArm>,
Expand Down
12 changes: 12 additions & 0 deletions genco-macros/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ impl<'a> Encoder<'a> {
} => {
self.encode_match(condition, arms);
}
Ast::Let { name, expr } => {
self.encode_let(name, expr);
}
}

Ok(())
Expand Down Expand Up @@ -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<LineColumn> {
// So we've (potentially) encountered the first ever token, while we
// have a spanned start like `quote_in! { out => foo }`, `foo` is now
Expand Down
18 changes: 18 additions & 0 deletions genco-macros/src/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Token![let]>()?;

let req = Requirements::default();

let name = syn::Pat::parse_single(input)?;
input.parse::<Token![=]>()?;
let expr = syn::Expr::parse_without_eager_brace(input)?;

let ast = Ast::Let { name, expr };

Ok((req, ast))
}

/// Parse evaluation: `[*]<binding> => <expr>`.
fn parse_scope(&self, input: ParseStream) -> Result<Ast> {
input.parse::<Token![ref]>()?;
Expand Down Expand Up @@ -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())? {
Expand Down
42 changes: 42 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,48 @@
///
/// <br>
///
/// # Variable assignment
///
/// You can use `$(let <binding> = <expr>)` 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>(())
/// ```
///
/// <br>
///
/// # Scopes
///
/// You can use `$(ref <binding> { <expr> })` to gain access to the current
Expand Down
85 changes: 85 additions & 0 deletions tests/test_token_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit db32718

Please sign in to comment.