zyn โ a template engine and framework for writing procedural macros
โ Rust ๐ 2026-03-06 ๐ค surdeus ๐๏ธ 2I've been working on zyn, a framework that tries to make writing proc macros less painful. Instead of assembling everything out of raw quote! blocks and manual attribute parsing, zyn gives you a small template DSL, reusable code-generation components, and typed attribute extraction.
It's at v0.2.0 and published on crates.io.
The problem
Writing a non-trivial proc macro typically means:
- Deeply nested
quote!blocks that become hard to follow - Re-implementing attribute parsing boilerplate for every crate
- Duplicated string transformation logic scattered across
format_ident!calls - No real way to break code generation into reusable pieces
What zyn provides
Templates with interpolation and pipes
Instead of quote! { fn #snake_name() {} }, you write:
zyn! {
fn {{ name | snake }}() {}
}
Pipes transform values inline โ snake, camel, pascal, screaming, kebab, upper, lower, str, trim:"_", plural, singular, ident:"get_{}", fmt:"{}!" โ and they compose:
zyn! {
const {{ name | snake | upper }}: &str = {{ name | fmt:"{}!" }};
}
Control flow
zyn! {
@if (input.is_pub) { pub }
@else { pub(crate) }
struct {{ input.ident }} {
@for (field in fields.iter()) {
{{ field.ident }}: {{ field.ty }},
}
}
}
Elements โ reusable code-generation components
This is the part I'm most excited about. #[zyn::element] lets you define parameterized, composable pieces of code generation and call them from templates:
#[zyn::element]
fn field_decl(
vis: syn::Visibility,
name: syn::Ident,
ty: syn::Type,
) -> zyn::TokenStream {
zyn::zyn! { {{ vis }} {{ name }}: {{ ty }}, }
}
zyn! {
struct {{ input.ident }} {
@for (field in fields.iter()) {
@field_decl(
vis = field.vis.clone(),
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
}
Elements can also accept children blocks, which makes wrapping patterns clean:
#[zyn::element]
fn wrapper(vis: syn::Visibility, children: zyn::TokenStream) -> zyn::TokenStream {
zyn::zyn! { {{ vis }} mod generated { {{ children }} } }
}
zyn! {
@wrapper(vis = input.vis.clone()) {
pub fn hello() {}
}
}
Typed attribute parsing
#[derive(zyn::Attribute)] generates a typed parser for your macro's attributes:
#[derive(zyn::Attribute)]
#[zyn("builder")]
struct BuilderConfig {
#[zyn(default = "build".to_string())]
method: String,
skip: bool,
}
// In your proc macro:
let cfg = BuilderConfig::from_input(&input)?;
Fields can be auto-resolved in elements without passing them explicitly:
#[zyn::element]
fn builder_method(
#[zyn(input)] cfg: zyn::Attr<BuilderConfig>,
name: syn::Ident,
) -> zyn::TokenStream {
let method = zyn::format_ident!("{}", cfg.method);
zyn::zyn! { pub fn {{ method }}(self) -> Self { self } }
}
Custom pipes
#[zyn::pipe]
fn prefix(input: String) -> syn::Ident {
syn::Ident::new(&format!("pfx_{}", input), zyn::Span::call_site())
}
zyn! { {{ name | prefix }} }
How it relates to existing crates
quote!โzynusesquoteinternally and thezyn!macro compiles down to the same token stream manipulation. You can mixzyn!andquote!freely.darlingโ#[derive(zyn::Attribute)]covers similar ground.darlingis more mature;zyn's version integrates directly with the element system.proc-macro-errorโzynhas its ownDiagnosticsaccumulator anderror!/warn!/bail!macros generated into element bodies.
Links
cargo add zyn- GitHub: GitHub - aacebo/zyn: A procedural macro development framework designed to simplify and add structure to rust macro development ยท GitHub
- Docs/book: Introduction - Zyn
I'd love feedback, especially on the element composition model โ it's the part most unlike anything else in the ecosystem and I'm curious whether it resonates with people who write complex proc macros.
1 post - 1 participant
๐ท๏ธ Rust_feed