A template engine and framework for Rust procedural macros.
zyn replaces the syn + quote + heck + proc-macro-error stack with a single
dependency. Write proc macros with templates, reusable elements, typed attribute
parsing, and chainable pipes.
Table of Contents
Templates
The zyn! macro is the core of zyn. Write token output as if it were source code,
with {{ }} interpolation and @ control flow directives.
Interpolation — any ToTokens value:
let name = format_ident!;
zyn!
// → fn hello_world() {}
Pipes — transform values inline:
zyn!
// name = "hello_world" → fn HelloWorld() {}
Control flow:
zyn!
Full template syntax:
| Syntax | Purpose |
|---|---|
{{ expr }} |
Interpolate any ToTokens value |
{{ expr | pipe }} |
Transform value through a pipe before inserting |
@if (cond) { ... } |
Conditional token emission |
@else { ... } |
Else branch |
@else if (cond) { ... } |
Else-if branch |
@for (x in iter) { ... } |
Loop over an iterator |
@for (N) { ... } |
Repeat N times |
@match (expr) { pat => { ... } } |
Pattern-based emission |
@element_name(prop = val) |
Invoke a #[element] component |
Elements
Elements are reusable template components defined with #[zyn::element].
They encapsulate a fragment of token output and accept typed props.
Define an element:
Invoke it inside any template with @:
zyn!
Elements can also receive extractors — values resolved automatically from proc macro
input — by marking a param with #[zyn(input)]:
// Applied to: struct User { first_name: String, age: u32 }
// Generates:
// impl User {
// pub fn get_first_name(&self) -> &String { &self.first_name }
// pub fn get_age(&self) -> &u32 { &self.age }
// }
Pipes
Pipes transform interpolated values: {{ expr | pipe }}. They chain left to right:
zyn!
// name = "HelloWorld" → fn get_hello_world() {}
Built-in pipes:
| Pipe | Input example | Output |
|---|---|---|
snake |
HelloWorld |
hello_world |
pascal |
hello_world |
HelloWorld |
camel |
hello_world |
helloWorld |
screaming |
HelloWorld |
HELLO_WORLD |
kebab |
HelloWorld |
"hello-world" |
upper |
hello |
HELLO |
lower |
HELLO |
hello |
str |
hello |
"hello" |
trim |
__foo__ |
foo |
plural |
user |
users |
singular |
users |
user |
ident:"pattern_{}" |
hello |
pattern_hello (ident) |
fmt:"pattern_{}" |
hello |
"pattern_hello" (string) |
Custom pipes via #[zyn::pipe]:
zyn!
// name = "hello" → fn HELLO_BANG() {}
Attributes
zyn provides two tools for attribute handling: a derive macro for typed parsing and a proc macro attribute for writing attribute macros.
Typed attribute structs via #[derive(Attribute)]:
// users write: #[builder(skip)] or #[builder(method = "create")]
The derive generates from_args, FromArg, and FromInput implementations, as well as
a human-readable about() string for error messages.
Attribute proc macros via #[zyn::attribute]:
Features
| Feature | Default | Description |
|---|---|---|
derive |
✓ | All proc macro attributes: #[element], #[pipe], #[derive], #[attribute], and #[derive(Attribute)] |
ext |
Extension traits for common syn AST types (AttrExt, FieldExt, TypeExt, etc.) |
|
pretty |
Pretty-printed token output in debug mode | |
diagnostics |
Error accumulation — collect multiple errors before aborting |
ext
The ext module adds ergonomic extension traits for navigating syn AST types.
= { = ["ext"] }
use ;
// check and read attribute arguments
if attr.is
// inspect field types
if field.is_option
pretty
The pretty feature enables pretty-printed output in debug mode, formatting
generated token streams as readable Rust source code via prettyplease.
= { = ["pretty"] }
Enable debug output per-element with the debug or debug = "pretty" argument,
then set ZYN_DEBUG="*" at build time:
ZYN_DEBUG="*"
note: zyn::element ─── my_element
struct MyElement {
pub name: zyn::syn::Ident,
}
impl ::zyn::Render for MyElement {
fn render(&self, input: &::zyn::Input) -> ::zyn::proc_macro2::TokenStream {
...
}
}
diagnostics
The diagnostics feature enables error accumulation — collecting multiple compiler
errors before aborting, so users see all problems at once instead of one at a time.
= { = ["diagnostics"] }
Inside any #[zyn::element], #[zyn::derive], or #[zyn::attribute] body, use the
built-in diagnostic macros directly — no setup required:
| Macro | Level | Behaviour |
|---|---|---|
error!(msg) |
error | accumulates, does not stop execution |
warn!(msg) |
warning | accumulates, does not stop execution |
note!(msg) |
note | accumulates, does not stop execution |
help!(msg) |
help | accumulates, does not stop execution |
bail!(msg) |
error | accumulates and immediately returns |
All accumulated diagnostics are emitted together at the end of the element or macro body, so users see every error at once instead of fixing them one by one.
error: reserved identifier `forbidden`
--> src/main.rs:3:1
error: reserved identifier `forbidden`
--> src/main.rs:7:1
Performance
Benchmarks confirm the zero-overhead claim: the full pipeline (parse, extract, codegen) matches vanilla syn + quote for both structs and enums. Where zyn replaces external crates, it's faster — case conversion is ~6x faster than heck, and attribute parsing is ~14% faster than darling.
Live benchmark charts on bencher.dev
Discussions
License
MIT