diff --git a/CHANGELOG.md b/CHANGELOG.md index c7989e4..072ec3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added "functions" functionality that allows to apply functions over arguments. - Made it possible to pass integers as arguments. - Added "upper()", "lower()", "snake_case()" and "camel_case()" functions for case-manipulation. +- Added "hash()" function that hashes an input value deterministically within the scope + of a single macro invocation. ### Changed diff --git a/README.md b/README.md index 6595b9b..2874ed7 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,20 @@ macro_rules! outer_macro { outer_macro!(foo); +macro_rules! global_var_macro { + () => { + // `my_static` is going to be unique in each invocation of `global_var_macro!()`. + // But within the same invocation `hash(1)` will yield the same result. + compose_idents!( + my_static = [foo, _, hash(1)]; { + static my_static: u32 = 42; + }); + }; +} + +global_var_macro!(); +global_var_macro!(); + assert_eq!(foo_baz(), 123); assert_eq!(spam_1_eggs(), 321); assert_eq!(nested_foo(), 42); diff --git a/snippets/usage.rs b/snippets/usage.rs index 4a2f06d..df688fa 100644 --- a/snippets/usage.rs +++ b/snippets/usage.rs @@ -33,6 +33,20 @@ macro_rules! outer_macro { outer_macro!(foo); +macro_rules! global_var_macro { + () => { + // `my_static` is going to be unique in each invocation of `global_var_macro!()`. + // But within the same invocation `hash(1)` will yield the same result. + compose_idents!( + my_static = [foo, _, hash(1)]; { + static my_static: u32 = 42; + }); + }; +} + +global_var_macro!(); +global_var_macro!(); + assert_eq!(foo_baz(), 123); assert_eq!(spam_1_eggs(), 321); assert_eq!(nested_foo(), 42); diff --git a/src/core.rs b/src/core.rs index ef5b869..ab2748a 100644 --- a/src/core.rs +++ b/src/core.rs @@ -2,6 +2,12 @@ use quote::ToTokens; use syn::parse::discouraged::Speculative; use syn::parse::{Parse, ParseStream}; +/// State of a particular macro invocation. +#[derive(Debug)] +pub struct State { + pub(super) seed: u64, +} + /// Argument in form of an identifier, underscore or a string literal. #[derive(Debug)] pub struct Arg { @@ -37,6 +43,7 @@ pub enum Func { Lower(Box), SnakeCase(Box), CamelCase(Box), + Hash(Box), } impl Func { @@ -60,10 +67,11 @@ impl Parse for Func { ("lower", 1) => Ok(Func::Lower(Box::new(args.drain(..).next().unwrap()))), ("snake_case", 1) => Ok(Func::SnakeCase(Box::new(args.drain(..).next().unwrap()))), ("camel_case", 1) => Ok(Func::CamelCase(Box::new(args.drain(..).next().unwrap()))), - _ => { - Err(input - .error(r#"Expected "upper()", "lower()", "snake_case()" or "camel_case()""#)) - } + ("hash", 1) => Ok(Func::Hash(Box::new(args.drain(..).next().unwrap()))), + _ => Err(input.error( + r#"Expected "upper()", "lower()", "snake_case()", + "camel_case()" or "hash()"."#, + )), } } } diff --git a/src/eval.rs b/src/eval.rs index aaa0146..8d0748c 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,33 +1,34 @@ -use crate::core::{Arg, Expr, Func}; -use crate::funcs::{to_camel_case, to_snake_case}; +use crate::core::{Arg, Expr, Func, State}; +use crate::funcs::{hash, to_camel_case, to_snake_case}; /// A syntactic structure that can be evaluated. pub trait Eval { - fn eval(&self) -> String; + fn eval(&self, state: &State) -> String; } impl Eval for Arg { - fn eval(&self) -> String { + fn eval(&self, _: &State) -> String { self.value.clone() } } impl Eval for Func { - fn eval(&self) -> String { + fn eval(&self, state: &State) -> String { match self { - Func::Upper(expr) => expr.eval().to_uppercase(), - Func::Lower(expr) => expr.eval().to_lowercase(), - Func::SnakeCase(expr) => to_snake_case(expr.eval().as_str()), - Func::CamelCase(expr) => to_camel_case(expr.eval().as_str()), + Func::Upper(expr) => expr.eval(state).to_uppercase(), + Func::Lower(expr) => expr.eval(state).to_lowercase(), + Func::SnakeCase(expr) => to_snake_case(expr.eval(state).as_str()), + Func::CamelCase(expr) => to_camel_case(expr.eval(state).as_str()), + Func::Hash(expr) => hash(expr.eval(state).as_str(), state), } } } impl Eval for Expr { - fn eval(&self) -> String { + fn eval(&self, state: &State) -> String { match self { - Expr::ArgExpr { value } => value.eval(), - Expr::FuncCallExpr { value } => value.eval(), + Expr::ArgExpr { value } => value.eval(state), + Expr::FuncCallExpr { value } => value.eval(state), } } } diff --git a/src/funcs.rs b/src/funcs.rs index 28930a4..b2a9d2f 100644 --- a/src/funcs.rs +++ b/src/funcs.rs @@ -1,3 +1,6 @@ +use crate::core::State; +use std::hash::{DefaultHasher, Hash, Hasher}; + /// Converts the input string to snake_case. pub fn to_snake_case(input: &str) -> String { let mut result = String::new(); @@ -38,10 +41,24 @@ pub fn to_camel_case(input: &str) -> String { result } +/// Generates an identifier from a provided seed deterministically within a single macro invocation. +/// +/// `hash(1)` called within a single macro invocation will always return the same +/// value but different in another macro invocation. +pub fn hash(input: &str, state: &State) -> String { + let mut hasher = DefaultHasher::new(); + state.seed.hash(&mut hasher); + input.hash(&mut hasher); + let hash = hasher.finish().to_string(); + let result = format!("__{}", hash); + result +} + #[cfg(test)] mod tests { use super::*; use rstest::rstest; + use syn::Ident; #[rstest] #[case("fooBar", "foo_bar")] @@ -72,4 +89,28 @@ mod tests { let actual = to_camel_case(input); assert_eq!(actual, expected, "Input: {}", input); } + + #[rstest] + fn test_random_valid_ident() { + let state = State { seed: 1 }; + let actual = hash("1", &state); + let ident_result = syn::parse_str::(actual.as_str()); + + assert!( + ident_result.is_ok(), + "Result: {},\nError: {}", + actual, + ident_result.unwrap_err(), + ); + } + + #[rstest] + fn test_random_determinism() { + let state = State { seed: 1 }; + let expected = hash("1", &state); + let actual = hash("1", &state); + + assert_eq!(actual, expected); + assert_ne!(hash("2", &state), expected); + } } diff --git a/src/lib.rs b/src/lib.rs index 22aa46a..4b9521b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod core; mod eval; mod funcs; -use crate::core::Expr; +use crate::core::{Expr, State}; use crate::eval::Eval; use proc_macro::TokenStream; use quote::{format_ident, quote}; @@ -23,9 +23,9 @@ struct IdentSpecItem { } impl IdentSpecItem { - fn replacement(&self) -> Ident { + fn replacement(&self, state: &State) -> Ident { let ident = self.exprs.iter().fold("".to_string(), |acc, item| { - format!("{}{}", acc, item.eval()) + format!("{}{}", acc, item.eval(state)) }); format_ident!("{}", ident) } @@ -65,10 +65,10 @@ impl Parse for IdentSpec { } impl IdentSpec { - fn replacements(&self) -> HashMap { + fn replacements(&self, state: &State) -> HashMap { self.items .iter() - .map(|item| (item.alias.clone(), item.replacement())) + .map(|item| (item.alias.clone(), item.replacement(state))) .collect() } } @@ -98,6 +98,8 @@ impl VisitMut for ComposeIdentsVisitor { } } +static mut COUNTER: u64 = 0; + /// Compose identifiers from the provided parts and replace their aliases in the code block. /// /// # Example @@ -107,9 +109,15 @@ impl VisitMut for ComposeIdentsVisitor { /// ``` #[proc_macro] pub fn compose_idents(input: TokenStream) -> TokenStream { + let state = State { + seed: unsafe { + COUNTER += 1; + COUNTER + }, + }; let args = parse_macro_input!(input as ComposeIdentsArgs); let mut visitor = ComposeIdentsVisitor { - replacements: args.spec.replacements(), + replacements: args.spec.replacements(&state), }; let mut block = args.block; visitor.visit_block_mut(&mut block); diff --git a/tests/compile/funcs/random.rs b/tests/compile/funcs/random.rs new file mode 100644 index 0000000..73d1fe8 --- /dev/null +++ b/tests/compile/funcs/random.rs @@ -0,0 +1,37 @@ +use compose_idents::compose_idents; + +compose_idents!( + my_unique_var = [foo, _, hash(1)]; + my_var = [SPAM, _, EGGS]; { + const my_unique_var: u32 = 42; + const my_var: u32 = my_unique_var; +}); + +compose_idents!( + my_unique_var = [foo, _, hash(1)]; + my_var = [BORK, _, GORK]; { + const my_unique_var: u32 = 42; + const my_var: u32 = my_unique_var; +}); + +macro_rules! my_macro { + () => { + compose_idents!( + my_local = [foo, _, hash(1)]; + my_same_local = [foo, _, hash(1)]; + my_other_local = [foo, _, hash(2)]; { + let my_local: u32 = 42; + let my_other_local: u32 = my_same_local; + let comparison: bool = my_local == my_other_local; + + assert!(comparison); + }); + }; +} + +fn main() { + assert_eq!(SPAM_EGGS, 42); + assert_eq!(BORK_GORK, 42); + + my_macro!(); +} diff --git a/tests/tests.rs b/tests/tests.rs index 01fede1..66b8a26 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -14,4 +14,5 @@ fn compile_tests() { t.pass("tests/compile/funcs/different_arg_types.rs"); t.pass("tests/compile/funcs/snake_case.rs"); t.pass("tests/compile/funcs/camel_case.rs"); + t.pass("tests/compile/funcs/random.rs"); }