Skip to content

Commit

Permalink
feat(funcs): introduce functions
Browse files Browse the repository at this point in the history
This commit adds an option to apply functions over regular arguments.
For now only upper() and lower() functions are available.
  • Loading branch information
AndreiPashkin committed Feb 7, 2025
1 parent bea8487 commit ef5ac23
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 23 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Docs/test fixes.

- Added "functions" functionality that allows to apply functions over arguments.

### Changed

Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ Here is how the macro works:
use compose_idents::compose_idents;


compose_idents!(my_fn_1 = [foo, _, "baz"]; my_fn_2 = [spam, _, eggs]; {
fn my_fn_1() -> u32 {
compose_idents!(
my_fn = [foo, _, "baz"];
MY_UPPER_STATIC = [upper(spam), _, upper("eggs")];
MY_LOWER_STATIC = [lower(GORK), _, lower(BORK)]; {
fn my_fn() -> u32 {
123
}

fn my_fn_2() -> u32 {
321
}
static MY_UPPER_STATIC: u32 = 321;
static MY_LOWER_STATIC: u32 = 321123;
});


assert_eq!(foo_baz(), 123);
assert_eq!(spam_eggs(), 321);
assert_eq!(SPAM_EGGS, 321);
assert_eq!(gork_bork, 321123);
```

Here is a more practical example for how to auto-generate names for macro-generated tests for different data types:
Expand Down
11 changes: 10 additions & 1 deletion examples/usage.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use compose_idents::compose_idents;

compose_idents!(my_fn_1 = [foo, _, "baz"]; my_fn_2 = [spam, _, eggs]; {
compose_idents!(
my_fn_1 = [foo, _, "baz"];
my_fn_2 = [spam, _, eggs];
my_const = [upper(foo), _, lower(BAR)];
my_static = [upper(lower(BAR))]; {
fn my_fn_1() -> u32 {
123
}

fn my_fn_2() -> u32 {
321
}

const my_const: u32 = 42;
static my_static: u32 = 42;
});

macro_rules! outer_macro {
Expand All @@ -26,4 +33,6 @@ fn main() {
assert_eq!(foo_baz(), 123);
assert_eq!(spam_eggs(), 321);
assert_eq!(nested_foo(), 42);
assert_eq!(FOO_bar, 42);
assert_eq!(BAR, 42);
}
80 changes: 80 additions & 0 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use quote::ToTokens;
use syn::parse::discouraged::Speculative;
use syn::parse::{Parse, ParseStream};

/// Argument in form of an identifier, underscore or a string literal.
#[derive(Debug)]
pub struct Arg {
pub(super) value: String,
}

impl Parse for Arg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let value: String;
if input.peek(syn::Ident) && !input.peek2(syn::token::Paren) {
let ident = input.parse::<syn::Ident>()?;
value = ident.to_string();
} else if input.parse::<syn::Token![_]>().is_ok() {
value = "_".to_string();
} else if let Ok(lit_str) = input.parse::<syn::LitStr>() {
value = lit_str.value();
} else {
return Err(input.error("Expected identifier or _"));
}
Ok(Arg { value })
}
}

/// Function call in form of `upper(arg)` or `lower(arg)`.
#[derive(Debug)]
pub enum Func {
Upper(Box<Expr>),
Lower(Box<Expr>),
}

impl Parse for Func {
fn parse(input: ParseStream) -> syn::Result<Self> {
let call = input.parse::<syn::ExprCall>()?;
let func_name = call.func.to_token_stream().to_string();
match func_name.as_str() {
"upper" | "lower" => {
let args = call.args;
if args.len() != 1 {
return Err(input.error("Expected 1 argument"));
}
let arg = syn::parse2::<Expr>(args.into_token_stream())?;
match func_name.as_str() {
"upper" => Ok(Func::Upper(Box::new(arg))),
"lower" => Ok(Func::Lower(Box::new(arg))),
_ => unreachable!(),
}
}
_ => Err(input.error(r#"Expected "upper()" or "lower()""#)),
}
}
}

/// Expression in form of an argument or a function call.
#[derive(Debug)]
pub(super) enum Expr {
ArgExpr { value: Box<Arg> },
FuncCallExpr { value: Box<Func> },
}

impl Parse for Expr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let fork = input.fork();
if let Ok(func) = fork.parse::<Func>() {
input.advance_to(&fork);
Ok(Expr::FuncCallExpr {
value: Box::new(func),
})
} else if let Ok(arg) = input.parse::<Arg>() {
Ok(Expr::ArgExpr {
value: Box::new(arg),
})
} else {
Err(input.error("Expected argument or function call"))
}
}
}
30 changes: 30 additions & 0 deletions src/eval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::core::{Arg, Expr, Func};

/// A syntactic structure that can be evaluated.
pub trait Eval {
fn eval(&self) -> String;
}

impl Eval for Arg {
fn eval(&self) -> String {
self.value.clone()
}
}

impl Eval for Func {
fn eval(&self) -> String {
match self {
Func::Upper(expr) => expr.eval().to_uppercase(),
Func::Lower(expr) => expr.eval().to_lowercase(),
}
}
}

impl Eval for Expr {
fn eval(&self) -> String {
match self {
Expr::ArgExpr { value } => value.eval(),
Expr::FuncCallExpr { value } => value.eval(),
}
}
}
30 changes: 15 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#![doc = include_str!("../README.md")]

mod core;
mod eval;

use crate::core::Expr;
use crate::eval::Eval;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use std::collections::HashMap;
Expand All @@ -8,20 +13,19 @@ use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
visit_mut::VisitMut,
Block, Ident, LitStr, Token,
Block, Ident, Token,
};

struct IdentSpecItem {
alias: Ident,
parts: Vec<String>,
exprs: Vec<Expr>,
}

impl IdentSpecItem {
fn replacement(&self) -> Ident {
let ident = self
.parts
.iter()
.fold("".to_string(), |acc, item| format!("{}{}", acc, item));
let ident = self.exprs.iter().fold("".to_string(), |acc, item| {
format!("{}{}", acc, item.eval())
});
format_ident!("{}", ident)
}
}
Expand All @@ -38,23 +42,19 @@ impl Parse for IdentSpec {
input.parse::<Token![=]>()?;
let content;
bracketed!(content in input);
let mut parts = Vec::new();
let mut exprs = Vec::new();
loop {
if let Ok(part) = content.parse::<Ident>() {
parts.push(part.to_string());
} else if content.parse::<Token![_]>().is_ok() {
parts.push("_".to_string());
} else if let Ok(part) = content.parse::<LitStr>() {
parts.push(part.value());
if let Ok(expr) = content.parse::<Expr>() {
exprs.push(expr);
} else {
return Err(content.error("Expected identifier or _"));
return Err(content.error("Invalid expression."));
}
if content.is_empty() {
break;
}
content.parse::<Token![,]>()?;
}
items.push(IdentSpecItem { alias, parts });
items.push(IdentSpecItem { alias, exprs });
input.parse::<Token![;]>()?;
if !input.peek(Ident) {
break;
Expand Down
9 changes: 9 additions & 0 deletions tests/compile/funcs/different_arg_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use compose_idents::compose_idents;

compose_idents!(my_var = [upper("foo"), lower(_), upper(bar)]; {
const my_var: u32 = 42;
});

fn main() {
assert_eq!(FOO_BAR, 42);
}
9 changes: 9 additions & 0 deletions tests/compile/funcs/lower.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use compose_idents::compose_idents;

compose_idents!(my_var = [lower(FOO), _, lower(BAR)]; {
const my_var: u32 = 42;
});

fn main() {
assert_eq!(foo_bar, 42);
}
9 changes: 9 additions & 0 deletions tests/compile/funcs/nested.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use compose_idents::compose_idents;

compose_idents!(my_var = [lower(upper(FOO))]; {
const my_var: u32 = 42;
});

fn main() {
assert_eq!(foo, 42);
}
9 changes: 9 additions & 0 deletions tests/compile/funcs/upper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use compose_idents::compose_idents;

compose_idents!(my_var = [upper(foo), _, upper(bar)]; {
const my_var: u32 = 42;
});

fn main() {
assert_eq!(FOO_BAR, 42);
}
4 changes: 4 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ fn compile_tests() {
t.pass("tests/compile/multi_compose.rs");
t.pass("tests/compile/const_var_compose.rs");
t.pass("tests/compile/generic_param_compose.rs");
t.pass("tests/compile/funcs/upper.rs");
t.pass("tests/compile/funcs/lower.rs");
t.pass("tests/compile/funcs/nested.rs");
t.pass("tests/compile/funcs/different_arg_types.rs");
}

0 comments on commit ef5ac23

Please sign in to comment.