Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for variable declarations in templates #55

Merged
merged 3 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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