Skip to content

Commit

Permalink
feat(funcs): introduce hash() function
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreiPashkin committed Feb 11, 2025
1 parent 6a6612b commit fd60d2b
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ compose_idents!(
my_const = [upper(foo), _, lower(BAR)];
my_static = [upper(lower(BAR))];
MY_SNAKE_CASE_STATIC = [snake_case(snakeCase)];
MY_CAMEL_CASE_STATIC = [camel_case(camel_case)]; {
MY_CAMEL_CASE_STATIC = [camel_case(camel_case)];
MY_UNIQUE_STATIC = [hash(0b11001010010111)]; {
fn my_fn_1() -> u32 {
123
}
Expand All @@ -51,6 +52,7 @@ compose_idents!(
static my_static: u32 = 42;
static MY_SNAKE_CASE_STATIC: u32 = 42;
static MY_CAMEL_CASE_STATIC: u32 = 42;
static MY_UNIQUE_STATIC: u32 = 42;
});

macro_rules! outer_macro {
Expand All @@ -65,6 +67,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);
Expand Down
18 changes: 17 additions & 1 deletion snippets/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ compose_idents!(
my_const = [upper(foo), _, lower(BAR)];
my_static = [upper(lower(BAR))];
MY_SNAKE_CASE_STATIC = [snake_case(snakeCase)];
MY_CAMEL_CASE_STATIC = [camel_case(camel_case)]; {
MY_CAMEL_CASE_STATIC = [camel_case(camel_case)];
MY_UNIQUE_STATIC = [hash(0b11001010010111)]; {
fn my_fn_1() -> u32 {
123
}
Expand All @@ -19,6 +20,7 @@ compose_idents!(
static my_static: u32 = 42;
static MY_SNAKE_CASE_STATIC: u32 = 42;
static MY_CAMEL_CASE_STATIC: u32 = 42;
static MY_UNIQUE_STATIC: u32 = 42;
});

macro_rules! outer_macro {
Expand All @@ -33,6 +35,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);
Expand Down
16 changes: 12 additions & 4 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -37,6 +43,7 @@ pub enum Func {
Lower(Box<Expr>),
SnakeCase(Box<Expr>),
CamelCase(Box<Expr>),
Hash(Box<Expr>),
}

impl Func {
Expand All @@ -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()"."#,
)),
}
}
}
Expand Down
25 changes: 13 additions & 12 deletions src/eval.rs
Original file line number Diff line number Diff line change
@@ -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),
}
}
}
41 changes: 41 additions & 0 deletions src/funcs.rs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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::<Ident>(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);
}
}
20 changes: 14 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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)
}
Expand Down Expand Up @@ -65,10 +65,10 @@ impl Parse for IdentSpec {
}

impl IdentSpec {
fn replacements(&self) -> HashMap<Ident, Ident> {
fn replacements(&self, state: &State) -> HashMap<Ident, Ident> {
self.items
.iter()
.map(|item| (item.alias.clone(), item.replacement()))
.map(|item| (item.alias.clone(), item.replacement(state)))
.collect()
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
37 changes: 37 additions & 0 deletions tests/compile/funcs/random.rs
Original file line number Diff line number Diff line change
@@ -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!();
}
1 change: 1 addition & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

0 comments on commit fd60d2b

Please sign in to comment.