A proc macro framework with templates, composable elements, and built-in diagnostics.
Table of Contents
Templates
Templates are fully type-checked at compile time — errors appear inline, just like regular Rust code.

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.
Auto-suggest
When a user misspells an argument name, zyn automatically suggests the closest known field. No extra setup required:
error: unknown argument `skiip`
--> src/main.rs:5:12
|
5 | #[builder(skiip)]
| ^^^^^
|
= help: did you mean `skip`?
Suggestions are offered when the edit distance is ≤ 3 characters. Distant or completely unknown keys produce only the "unknown argument" error without a suggestion.
Attribute proc macros via #[zyn::attribute]:
Testing
Assertions
zyn! returns Output — test both tokens and diagnostics directly:
Diagnostic assertions check error messages from error!, warn!, bail!:
| Macro | Purpose |
|---|---|
assert_tokens! |
Compare two token streams |
assert_tokens_empty! |
Assert no tokens produced |
assert_tokens_contain! |
Check for substring in output |
assert_diagnostic_error! |
Assert error diagnostic with message |
assert_diagnostic_warning! |
Assert warning diagnostic |
assert_diagnostic_note! |
Assert note diagnostic |
assert_diagnostic_help! |
Assert help diagnostic |
assert_compile_error! |
Alias for assert_diagnostic_error! |
With the pretty feature:
| Macro | Purpose |
|---|---|
assert_tokens_pretty! |
Compare using prettyplease-formatted output |
assert_tokens_contain_pretty! |
Substring check on pretty-printed output |
Debugging
Add debug (or debug = "pretty") to any zyn attribute macro and set ZYN_DEBUG to inspect generated code at compile time:
ZYN_DEBUG="*"
note: zyn::element ─── Greeting
struct Greeting { pub name : zyn :: syn :: Ident , } impl :: zyn :: Render
for Greeting { fn render (& self , input : & :: zyn :: Input) -> :: zyn ::
proc_macro2 :: TokenStream { ... } }
With the pretty feature, use debug = "pretty" for formatted output:
note: zyn::element ─── Greeting
struct Greeting {
pub name: zyn::syn::Ident,
}
impl ::zyn::Render for Greeting {
fn render(&self, input: &::zyn::Input) -> ::zyn::Output { ... }
}
All macros support debug: #[zyn::element], #[zyn::pipe], #[zyn::derive], #[zyn::attribute].
ZYN_DEBUG accepts comma-separated *-wildcard patterns matched against the generated PascalCase type name:
ZYN_DEBUG="Greeting" ZYN_DEBUG="Greet*" ZYN_DEBUG="*Element" ZYN_DEBUG="Greeting,Shout"
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
Pretty-printed debug output and assert_tokens_pretty! / assert_tokens_contain_pretty! assertion macros via prettyplease. See Debugging for usage.
= { = ["pretty"] }

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