Introduction
Zyn is a template engine and framework for Rust procedural macros. One crate replaces the patchwork of syn, quote, case-conversion libraries, diagnostic helpers, and attribute-parsing boilerplate that every proc macro project ends up assembling from scratch.
zyn! {
@for (field in fields.iter()) {
pub fn {{ field.ident | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
Why?
The problem
Building a proc macro in Rust today means pulling in a handful of loosely related crates and gluing them together yourself. Here’s what each pain point looks like — and how zyn solves it.
1. quote! has no control flow
Every conditional or loop forces you out of the template and back into Rust.
| ❌ Before | ✅ After |
|---|---|
|
|
2. Case conversion needs external crates
Renaming an identifier means importing heck, calling a conversion function, then wrapping it in format_ident!.
| ❌ Before | ✅ After |
|---|---|
|
|
3. Diagnostics are fragmented
Errors, warnings, notes, and help messages each use a different mechanism — or aren’t possible at all.
| ❌ Before | ✅ After |
|---|---|
|
|
4. Attribute parsing is reinvented every time
Every project writes its own parser for #[my_attr(skip, rename = "foo")].
| ❌ Before | ✅ After |
|---|---|
|
|
5. Reusable codegen means manual helper functions
There’s no composition model — just functions returning TokenStream.
| ❌ Before | ✅ After |
|---|---|
|
|
6. Five crates doing five things
| ❌ Before | ✅ After |
|---|---|
|
|
What zyn does differently
Zyn replaces the entire stack with a single zyn! template macro and a set of companion tools:
zyn! {
@for (field in fields.iter()) {
pub fn {{ field.ident | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
| Concern | Zyn approach |
|---|---|
| Code generation | zyn! template with {{ }} interpolation — reads like the code it generates |
| Control flow | @if, @for, @match directives inline — no .iter().map().collect() |
| Case conversion | Built-in pipes: {{ name | snake }}, {{ name | pascal }}, {{ name | screaming }} — no extra crate |
| Name formatting | {{ name | ident:"get_{}" }} — one expression, no let binding |
| Diagnostics | error!, warn!, note!, help!, bail! macros in #[zyn::element] bodies — one API for all diagnostic levels |
| Attribute parsing | #[derive(Attribute)] for typed attribute structs — built-in, no darling dependency |
| Reusable codegen | #[zyn::element] — composable template components invoked with @name(props) |
| Value transforms | #[zyn::pipe] — custom pipes that chain with built-ins |
| Proc macro entry points | #[zyn::derive] and #[zyn::attribute] — replace #[proc_macro_derive]/#[proc_macro_attribute] with auto-parsed Input and diagnostics |
| Debugging | zyn::debug! — drop-in zyn! replacement that prints the expansion (pretty, raw, ast modes) |
| String output | {{ name | str }} — stringify to a LitStr without ceremony |
One dependency. No runtime cost. Everything expands at compile time into the same TokenStream-building code you’d write by hand — just without the boilerplate.
Features
- Interpolation —
{{ expr }}inserts anyToTokensvalue, with field access and method calls - Pipes —
{{ name | snake }},{{ name | ident:"get_{}" }},{{ name | str }}— 13 built-in pipes plus custom - Control flow —
@if,@for,@matchwith full nesting - Diagnostics —
error!,warn!,note!,help!,bail!macros in#[zyn::element]bodies - Elements — reusable template components via
#[zyn::element] - Custom pipes — define transforms with
#[zyn::pipe] - Proc macro entry points —
#[zyn::derive]and#[zyn::attribute]with auto-parsed input, extractors, and diagnostics - Debugging —
zyn::debug!withpretty,raw, andastmodes - Attribute parsing —
#[derive(Attribute)]for typed attribute structs - Case conversion —
snake,camel,pascal,screaming,kebab,upper,lower,trim,plural,singular
Getting Started
Installation
Add zyn to your proc-macro crate:
[dependencies]
zyn = "0.3"
Zyn re-exports syn, quote, and proc-macro2 — you don’t need to add them separately. Access them as zyn::syn, zyn::quote, and zyn::proc_macro2.
Feature flags
| Feature | Default | Description |
|---|---|---|
derive | yes | Proc macros (zyn!, debug!, #[zyn::element], #[zyn::pipe], #[zyn::derive], #[zyn::attribute]), extractors (Extract<T>, Attr<T>, Fields, Variants, Data<T>), diagnostics (error!, warn!, note!, help!, bail!), and #[derive(Attribute)] |
ext | no | AttrExt and AttrsExt traits for parsing syn::Attribute values |
To enable ext:
[dependencies]
zyn = { version = "0.3", features = ["ext"] }
Your first template
The zyn! macro is a template engine that returns a zyn::TokenStream. Everything outside {{ }} and @ directives passes through as literal tokens, just like quote!:
use zyn::prelude::*;
let name = &input.ident;
let tokens: zyn::TokenStream = zyn! {
pub struct {{ name }}Builder {
ready: bool,
}
};
{{ expr }} interpolates any value that implements ToTokens — identifiers, types, expressions, even other token streams.
Pipes
Pipes transform interpolated values inline with |:
zyn! {
pub fn {{ name | snake }}(&self) -> &Self {
&self
}
}
Chain multiple pipes and format identifiers:
{{ name | snake | ident:"get_{}" }}
Built-in pipes include snake, camel, pascal, screaming, kebab, upper, lower, plural, singular, trim, str, ident, and fmt. See Pipes for the full list.
Control flow
Templates support @if, @for, and @match directives:
zyn! {
impl {{ ident }} {
@for (field in fields.iter()) {
pub fn {{ field.ident | snake }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
}
Conditionals:
zyn! {
@if (is_pub) { pub } struct {{ name }} {
@for (field in fields.iter()) {
@if (field.vis == syn::Visibility::Public(Default::default())) {
{{ field.ident }}: {{ field.ty }},
}
}
}
}
See Control Flow for @match and nested directives.
Elements
When a template pattern repeats, extract it into a reusable element with #[zyn::element]:
#[zyn::element]
fn getter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
zyn::zyn! {
pub fn {{ name | snake | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
}
}
Invoke elements inside templates with @:
zyn! {
impl {{ ident }} {
@for (field in fields.iter()) {
@getter(
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
}
Elements are compiled like function calls — they accept typed parameters and return a TokenStream. See Elements for children, extractors, and diagnostics.
Wiring it up
Use #[zyn::derive] to turn your templates into a real proc macro. Parameters marked #[zyn(input)] are automatically extracted from the derive input:
#[zyn::element]
fn getter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
zyn::zyn! {
pub fn {{ name | snake | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
}
}
#[zyn::derive]
fn my_getters(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields,
) -> zyn::TokenStream {
zyn::zyn! {
impl {{ ident }} {
@for (field in fields.iter()) {
@getter(
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
}
}
Users apply it like any derive macro — the function name my_getters becomes MyGetters:
#[derive(MyGetters)]
struct User {
name: String,
age: u32,
}
// Generated:
// impl User {
// pub fn get_name(&self) -> &String { &self.name }
// pub fn get_age(&self) -> &u32 { &self.age }
// }
See Proc Macro Entry Points for #[zyn::attribute], custom names, helper attributes, and more.
Next steps
- Templates — interpolation, control flow, pipes, case conversion
- Elements — children, extractors, input context, advanced patterns
- Diagnostics —
error!,warn!,note!,help!,bail! - derive(Attribute) — typed attribute parsing with
#[derive(Attribute)] - Proc Macro Entry Points —
#[zyn::derive]and#[zyn::attribute] - Debugging —
zyn::debug!withpretty,raw, andastmodes
Templates
The zyn! macro is the core of zyn. It accepts a template and returns a zyn::TokenStream:
let tokens: zyn::TokenStream = zyn! {
pub struct {{ name }} {
@for (field in fields.iter()) {
{{ field.ident }}: {{ field.ty }},
}
}
};
Everything outside {{ }} and @ directives passes through as literal tokens, just like quote!.
Syntax Overview
| Syntax | Purpose |
|---|---|
{{ expr }} | Interpolation — insert any ToTokens value |
{{ expr | pipe }} | Pipes — transform values (case conversion, formatting) |
@if / @for / @match | Control flow — conditionals, loops, pattern matching |
@element_name(props) | Element invocation — reusable template components |
Interpolation
Insert any expression that implements ToTokens with double braces:
zyn! { fn {{ name }}() -> {{ ret_type }} {} }
// output: fn hello() -> String {}
Field Access
Dot notation works inside interpolation:
zyn! {
{{ item.field.name }}: {{ item.field.ty }},
}
// output: age: u32,
Method Calls
zyn! {
{{ names.len() }}
}
// output: 3
Groups
Interpolation works inside parenthesized and bracketed groups:
zyn! { fn foo(x: {{ ty }}) }
// output: fn foo(x: u32)
zyn! { type Foo = [{{ ty }}; 4]; }
// output: type Foo = [u32; 4];
Control Flow
All control flow directives start with @.
@if
Conditionally include tokens. The condition is any Rust expression that evaluates to bool:
zyn! {
@if (is_async) {
async fn {{ name }}() {}
} @else if (is_unsafe) {
unsafe fn {{ name }}() {}
} @else {
fn {{ name }}() {}
}
}
Conditions can use field access and method calls:
zyn! {
@if (opts.is_pub) { pub }
@if (!fields.is_empty()) {
impl {{ name }} {
pub fn len(&self) -> usize { {{ fields.len() }} }
}
}
}
A common pattern is toggling pub based on a flag:
let is_pub = true;
let name = &input.ident;
zyn! {
@if (is_pub) { pub } fn {{ name }}() {}
}
// output: pub fn my_fn() {}
@if and {{ }} compose freely:
zyn! {
@if (field.is_optional) {
pub {{ field.name }}: Option<{{ field.ty }}>,
} @else {
pub {{ field.name }}: {{ field.ty }},
}
}
@for
Iterate over any value that produces an iterator:
zyn! {
@for (name in names) {
pub {{ name }}: f64,
}
}
// output: pub x: f64, pub y: f64, pub z: f64,
The iterator expression can be any Rust expression:
let field_names = fields.iter().map(|f| f.ident.clone().unwrap());
zyn! {
@for (name in field_names) {
pub {{ name }}: f64,
}
}
Iterating Over Struct Fields
zyn! {
impl {{ input.ident }} {
@for (field in input.fields.iter()) {
pub fn {{ field.ident | snake }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
}
.enumerate()
zyn! {
@for ((i, variant) in variants.iter().enumerate()) {
const {{ variant.ident | screaming }}: usize = {{ i }};
}
}
// output: const RED: usize = 0; const GREEN: usize = 1; const BLUE: usize = 2;
Filtering
zyn! {
@for (field in fields.iter().filter(|f| f.is_pub)) {
{{ field.ident }}: {{ field.ty }},
}
}
Count-based Loops
@for also accepts a count expression without a binding:
zyn! {
@for (3) { x, }
}
// output: x, x, x,
For indexed access, use a range:
zyn! {
@for (i in 0..fields.len()) {
{{ fields[i].ident }}: {{ fields[i].ty }},
}
}
Comma-separated Expansion
In quote!, repeating with separators uses #(#items),*. In zyn, put the separator in the loop:
zyn! {
fn new(
@for (field in fields.iter()) {
{{ field.ident }}: {{ field.ty }},
}
) -> Self {
Self {
@for (field in fields.iter()) {
{{ field.ident }},
}
}
}
}
Empty Iterators
If the iterator is empty, the body emits nothing — no error.
@match
Generate different code based on a value:
zyn! {
@match (kind) {
Kind::Struct => { struct {{ name }} {} }
Kind::Enum => { enum {{ name }} {} }
_ => {}
}
}
Expression Subjects
zyn! {
@match (value.len()) {
0 => { compile_error!("at least one field is required"); }
1 => { struct {{ name }}({{ fields[0].ty }}); }
_ => {
struct {{ name }} {
@for (field in fields.iter()) {
pub {{ field.ident }}: {{ field.ty }},
}
}
}
}
}
String Patterns
zyn! {
@match (repr.as_str()) {
"u8" => { impl From<{{ name }}> for u8 { fn from(v: {{ name }}) -> u8 { v.0 } } }
"u16" => { impl From<{{ name }}> for u16 { fn from(v: {{ name }}) -> u16 { v.0 } } }
"u32" => { impl From<{{ name }}> for u32 { fn from(v: {{ name }}) -> u32 { v.0 } } }
_ => { compile_error!("unsupported repr"); }
}
}
Multiple Patterns per Arm
zyn! {
@match (kind) {
Kind::Add | Kind::Sub => { fn apply(a: i32, b: i32) -> i32 { a {{ op }} b } }
Kind::Mul | Kind::Div => { fn apply(a: f64, b: f64) -> f64 { a {{ op }} b } }
}
}
Nesting
All directives nest freely:
zyn! {
@for (variant in variants.iter()) {
@if (variant.is_enabled) {
@match (variant.kind) {
VariantKind::Unit => {
{{ variant.name }},
}
VariantKind::Tuple => {
{{ variant.name }}({{ variant.ty }}),
}
VariantKind::Struct => {
{{ variant.name }} {
@for (field in variant.fields.iter()) {
{{ field.name }}: {{ field.ty }},
}
},
}
}
}
}
}
Pipes
Pipes transform interpolated values. Add them after a |:
zyn! {
fn {{ name | snake }}() {}
}
// output: fn hello_world() {}
Pipe names are written in snake_case in templates — they resolve to PascalCase structs automatically.
Built-in Pipes
| Pipe | Input | Output | Example |
|---|---|---|---|
upper | HelloWorld | HELLOWORLD | {{ name | upper }} |
lower | HELLO | hello | {{ name | lower }} |
snake | HelloWorld | hello_world | {{ name | snake }} |
camel | hello_world | helloWorld | {{ name | camel }} |
pascal | hello_world | HelloWorld | {{ name | pascal }} |
screaming | HelloWorld | HELLO_WORLD | {{ name | screaming }} |
kebab | HelloWorld | "hello-world" | {{ name | kebab }} |
str | hello | "hello" | {{ name | str }} |
trim | __foo__ | foo | {{ name | trim }} |
plural | User | Users | {{ name | plural }} |
singular | users | user | {{ name | singular }} |
Warning
kebabandstrreturn string literals, not identifiers, because their output may contain characters invalid in Rust identifiers.
Chaining
Pipes can be chained. Each pipe receives the output of the previous one:
zyn! { {{ name | snake | upper }} }
// HelloWorld -> hello_world -> HELLO_WORLD
Format Pipes
The ident and fmt pipes take a format pattern via : syntax. Use {} as the placeholder:
zyn! {
fn {{ name | ident:"get_{}" }}() {} // hello -> get_hello (as ident)
fn {{ name | ident:"{}_impl" }}() {} // hello -> hello_impl (as ident)
const NAME: &str = {{ name | fmt:"{}" }}; // hello -> "hello" (as string literal)
}
ident produces an identifier, fmt produces a string literal.
Combine with case pipes:
zyn! { {{ name | snake | ident:"get_{}" }} }
// HelloWorld -> hello_world -> get_hello_world
Custom Pipes
Define custom pipes with #[zyn::pipe]. The first parameter is the input, the return type is the output:
#[zyn::pipe]
fn prefix(input: String) -> syn::Ident {
syn::Ident::new(
&format!("pfx_{}", input),
zyn::Span::call_site(),
)
}
This generates a unit struct Prefix implementing the Pipe trait. Use it by its snake_case name:
zyn! { {{ name | prefix }} }
// hello -> pfx_hello
Custom Names
Override the template name:
#[zyn::pipe("yell")]
fn make_loud(input: String) -> syn::Ident {
syn::Ident::new(
&format!("{}__LOUD", input.to_uppercase()),
zyn::Span::call_site(),
)
}
zyn! { {{ name | yell }} }
// hello -> HELLO__LOUD
Chaining with Built-ins
Custom pipes chain with built-in pipes:
zyn! { {{ name | snake | prefix }} }
// HelloWorld -> hello_world -> pfx_hello_world
Case Conversion
Zyn provides case conversion utilities that work both inside and outside templates.
Functions
The zyn::case module has standalone conversion functions:
use zyn::case;
case::to_snake("HelloWorld") // "hello_world"
case::to_pascal("hello_world") // "HelloWorld"
case::to_camel("hello_world") // "helloWorld"
case::to_screaming("HelloWorld") // "HELLO_WORLD"
case::to_kebab("HelloWorld") // "hello-world"
Macros
Case conversion macros work with strings, syn::Ident, and token streams:
// String input -> String output
zyn::pascal!("hello_world") // "HelloWorld"
zyn::snake!("HelloWorld") // "hello_world"
// Ident input -> Ident output
zyn::pascal!(ident => ident) // syn::Ident in PascalCase
zyn::snake!(ident => ident) // syn::Ident in snake_case
// Ident input -> TokenStream output
zyn::pascal!(ident => token_stream)
In Templates
Inside zyn!, use the equivalent pipes:
zyn! {
fn {{ name | snake }}() {}
struct {{ name | pascal }} {}
const {{ name | screaming }}: &str = "";
}
See Pipes for the full list.
Elements
Elements are reusable template components. Define them with #[zyn::element] and invoke them with @ in templates.
Defining
Annotate a function with #[zyn::element]. Parameters become struct fields (props); the function must return zyn::TokenStream:
#[zyn::element]
fn field_decl(vis: syn::Visibility, name: syn::Ident, ty: syn::Type) -> zyn::TokenStream {
zyn::zyn! {
{{ vis }} {{ name }}: {{ ty }},
}
}
The macro generates a struct and a Render impl:
pub struct FieldDecl {
pub vis: syn::Visibility,
pub name: syn::Ident,
pub ty: syn::Type,
}
impl zyn::Render for FieldDecl {
fn render(&self, input: &zyn::Input) -> zyn::TokenStream {
let vis = &self.vis;
let name = &self.name;
let ty = &self.ty;
zyn::zyn! { {{ vis }} {{ name }}: {{ ty }}, }
}
}
The function name (snake_case) becomes the template directive name. The struct name is the PascalCase equivalent — field_decl → FieldDecl.
Elements are always infallible. For error handling, see Diagnostics.
Invoking
Reference an element by its snake_case name prefixed with @. Props are passed as name = value pairs:
zyn! {
@field_decl(
vis = field.vis.clone(),
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
// output: pub age: u32,
Prop values are raw Rust expressions — any expression that produces the right type works. Trailing commas are allowed. Elements can be invoked as many times as needed:
zyn! {
@for (field in fields.iter()) {
@field_decl(
vis = field.vis.clone(),
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
The input Parameter
Every element’s render body has an input: &zyn::Input in scope. This is the proc macro input context — the item being annotated:
#[zyn::element]
fn my_element(name: syn::Ident) -> zyn::TokenStream {
let ident = input.ident();
zyn::zyn! { /* ... */ }
}
When using #[zyn::derive] or #[zyn::attribute], input is provided automatically. For manual usage, define let input: zyn::Input = ...; before calling zyn!:
let input: zyn::Input = real_derive_input.into();
zyn::zyn! {
@my_element(name = ident)
}
See Proc Macro Entry Points for the recommended approach.
Extractor Params
Parameters marked with #[zyn(input)] are automatically resolved from the input context — they are not props and are not passed at the call site:
#[zyn::element]
fn my_element(
#[zyn(input)] cfg: zyn::Attr<MyConfig>,
#[zyn(input)] fields: zyn::Fields,
label: syn::Ident,
) -> zyn::TokenStream {
zyn::zyn! { /* cfg, fields, label all available */ }
}
Call site — only props are passed:
@my_element(label = some_ident)
See Extractors for the full list of extractor types.
Children
Children Parameter
Elements can accept children by including a children: zyn::TokenStream parameter:
#[zyn::element]
fn wrapper(name: syn::Ident, children: zyn::TokenStream) -> zyn::TokenStream {
zyn::zyn!(struct {{ name }} { {{ children }} })
}
zyn! {
@wrapper(name = input.ident.clone()) {
x: i32,
}
}
// output: struct Foo { x: i32, }
Children-Only Elements
Children-only elements can omit parentheses entirely:
#[zyn::element]
fn container(children: zyn::TokenStream) -> zyn::TokenStream {
zyn::zyn!(mod inner { {{ children }} })
}
zyn! {
@container {
struct Foo;
}
}
Zero-Parameter Elements
Elements with no parameters can be invoked without parentheses:
#[zyn::element]
fn divider() -> zyn::TokenStream {
zyn::zyn!(const DIVIDER: &str = "---";)
}
zyn! { @divider }
zyn! { @divider() } // also valid
Both forms are equivalent — the () is optional when there are no props.
Zero-parameter elements are useful for shared boilerplate:
#[zyn::element]
fn derive_common() -> zyn::TokenStream {
zyn::zyn!(#[derive(Debug, Clone, PartialEq)])
}
zyn! {
@derive_common
pub struct {{ name }} {
@for (field in fields.iter()) {
{{ field.ident }}: {{ field.ty }},
}
}
}
Zero-parameter elements can still accept children:
#[zyn::element]
fn section(children: zyn::TokenStream) -> zyn::TokenStream {
zyn::zyn! { pub mod section { {{ children }} } }
}
zyn! {
@section {
pub struct Foo;
pub struct Bar;
}
}
Diagnostics
The #[zyn::element] attribute generates error!, warn!, note!, help!, and bail! macros that push diagnostics to an auto-injected diagnostics accumulator.
error! — Compile Error
#[zyn::element]
fn validated(#[zyn(input)] ident: syn::Ident) -> zyn::TokenStream {
if ident.to_string() == "forbidden" {
error!("reserved identifier");
}
bail!();
zyn::zyn! { fn {{ ident }}() {} }
}
error: reserved identifier
--> src/lib.rs:8:24
warn! — Compiler Warning
Does not halt compilation:
#[zyn::element]
fn legacy(#[zyn(input)] ident: syn::Ident) -> zyn::TokenStream {
warn!("this usage is deprecated, use `new_api` instead");
zyn::zyn! { fn {{ ident }}() {} }
}
note! — Informational Note
note!("only named fields are supported");
note!("expected `{}`", expected; span = ident.span());
help! — Help Suggestion
help!("consider using `Builder::new()` instead");
help!("try `{}` instead", suggestion; span = ident.span());
bail! — Early Return on Errors
Returns early if any errors have accumulated, or pushes an error and returns immediately:
bail!(); // return if any errors
bail!("struct must have fields"); // push error + return
Errors with Notes and Help
Accumulate multiple diagnostics before returning:
#[zyn::element]
fn validated(name: syn::Ident) -> zyn::TokenStream {
if name.to_string() == "forbidden" {
error!("reserved identifier"; span = name.span());
note!("this name is reserved by the compiler");
help!("try a different name like `my_handler`");
}
bail!();
zyn::zyn! { fn {{ name }}() {} }
}
error: reserved identifier
note: this name is reserved by the compiler
help: try a different name like `my_handler`
--> src/lib.rs:8:24
|
8 | @validated(name = forbidden)
| ^^^^^^^^^
Format String Interpolation
All macros accept format!-style arguments:
error!("field `{}` is required", name);
warn!("type `{}` is deprecated", ty);
Custom Spans
Override the default span with ; span = expr:
error!("invalid field"; span = field.span());
bail!("missing `{}`", name; span = ident.span());
Accessing the Accumulator Directly
The diagnostics variable is a zyn::Diagnostics and can be used directly:
#[zyn::element]
fn my_element(#[zyn(input)] fields: zyn::Fields<syn::Field>) -> zyn::TokenStream {
for field in fields.iter() {
if field.ident.is_none() {
error!("all fields must be named"; span = field.span());
}
}
if diagnostics.has_errors() {
return diagnostics.emit();
}
zyn::zyn! { struct Validated; }
}
Extractors
Extractors are types implementing FromInput that pull structured data from a proc macro Input. Mark element params with #[zyn(input)] to auto-resolve them — they won’t be passed at the call site.
#[zyn::element]
fn my_element(
#[zyn(input)] item: syn::DeriveInput,
#[zyn(input)] fields: zyn::Fields,
label: syn::Ident, // prop — passed at @call site
) -> zyn::TokenStream {
zyn::zyn! { /* ... */ }
}
All wrapper extractors implement Deref/DerefMut to their inner type and provide an inner(self) -> T method.
Extractor Types
| Type | Extracts | Use Case |
|---|---|---|
Attr<T> | T::from_input(input) | #[derive(Attribute)] structs |
Extract<T> | T::from_input(input) | General FromInput wrapper |
Fields<T> | Struct fields | Defaults to syn::Fields |
Variants | Enum variants | Vec<syn::Variant> |
Data<T> | Derive data | Defaults to syn::Data |
Attr<T>
For attribute structs created with #[derive(Attribute)]:
#[zyn::element]
fn my_element(
#[zyn(input)] cfg: zyn::Attr<MyConfig>,
name: syn::Ident,
) -> zyn::TokenStream {
if cfg.skip { return zyn::TokenStream::new(); }
zyn::zyn! { /* ... */ }
}
Extract<T>
General-purpose wrapper for any FromInput type:
#[zyn::element]
fn my_element(
#[zyn(input)] generics: zyn::Extract<syn::Generics>,
) -> zyn::TokenStream {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
zyn::zyn! { /* ... */ }
}
Fields<T>
Extracts syn::Fields from a struct input. Generic over T: FromFields:
T | Behaviour |
|---|---|
syn::Fields (default) | Returns any field shape |
syn::FieldsNamed | Errors if not named fields |
syn::FieldsUnnamed | Errors if not unnamed fields |
#[zyn::element]
fn struct_element(
#[zyn(input)] fields: zyn::Fields,
) -> zyn::TokenStream {
zyn::zyn!(const COUNT: usize = {{ fields.len() }};)
}
With a specific field kind:
#[zyn::element]
fn named_struct_element(
#[zyn(input)] fields: zyn::Fields<syn::FieldsNamed>,
) -> zyn::TokenStream {
let names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
zyn::zyn! { /* ... */ }
}
Can also be used outside elements:
let fields = zyn::Fields::from_input(&input)?;
Variants
Extracts Vec<syn::Variant> from an enum input. Errors if not an enum:
#[zyn::element]
fn variant_names(
#[zyn(input)] variants: zyn::Variants,
) -> zyn::TokenStream {
zyn::zyn! {
@for (v in variants.iter()) {
const {{ v.ident | screaming }}: &str = {{ v.ident | str }};
}
}
}
Data<T>
Extracts syn::Data from a derive input. Generic over T: FromData. Only works with derive inputs:
T | Behaviour |
|---|---|
syn::Data (default) | Returns any data kind |
syn::DataStruct | Errors if not a struct |
syn::DataEnum | Errors if not an enum |
syn::DataUnion | Errors if not a union |
#[zyn::element]
fn struct_data_element(
#[zyn(input)] data: zyn::Data<syn::DataStruct>,
) -> zyn::TokenStream {
zyn::zyn!(const FIELD_COUNT: usize = {{ data.fields.len() }};)
}
Syn Types as Extractors
FromInput is implemented for syn types directly — use them as #[zyn(input)] params:
Derive Input Types
| Type | Matches |
|---|---|
syn::DeriveInput | Any derive input |
syn::DataStruct | Struct data only |
syn::DataEnum | Enum data only |
syn::DataUnion | Union data only |
Item Input Types
| Type | Matches |
|---|---|
syn::Item | Any item |
syn::ItemFn | fn |
syn::ItemStruct | struct |
syn::ItemEnum | enum |
syn::ItemUnion | union |
syn::ItemTrait | trait |
syn::ItemImpl | impl |
syn::ItemType | type |
syn::ItemMod | mod |
syn::ItemConst | const |
syn::ItemStatic | static |
syn::ItemUse | use |
syn::ItemExternCrate | extern crate |
syn::ItemForeignMod | extern "C" |
Cross-Input Extraction
syn::ItemStruct, syn::ItemEnum, and syn::ItemUnion also work with derive inputs — zyn reconstructs the item type from the derive data:
#[zyn::element]
fn struct_element(
#[zyn(input)] s: syn::ItemStruct,
) -> zyn::TokenStream {
// Works whether input is Input::Item or Input::Derive
zyn::zyn! { /* ... */ }
}
All other item types require an Input::Item context.
Input
Input Enum
Input is the unified proc macro input context — it wraps either a syn::DeriveInput or syn::Item and provides common accessors:
pub enum Input {
Derive(syn::DeriveInput),
Item(syn::Item),
}
impl Input {
pub fn attrs(&self) -> &[syn::Attribute];
pub fn ident(&self) -> &syn::Ident;
pub fn generics(&self) -> &syn::Generics;
pub fn vis(&self) -> &syn::Visibility;
}
Convert from standard syn types:
// From a derive macro input:
let input: zyn::Input = zyn::parse_input!(ts as syn::DeriveInput).into();
// From an attribute macro input:
let input: zyn::Input = zyn::parse_input!(ts as syn::Item).into();
// Parse directly:
let input: zyn::Input = zyn::parse!(token_stream)?;
Input implements Default, Parse, ToTokens, and syn::spanned::Spanned.
FromInput Trait
pub trait FromInput: Sized {
fn from_input(input: &Input) -> zyn::Result<Self>;
}
Returns zyn::Result<Self> — an alias for Result<Self, Diagnostics>. Errors are accumulated as Diagnostics instead of short-circuiting.
Implemented by:
| Type | Extracts |
|---|---|
#[derive(Attribute)] structs | Named attribute from input.attrs() |
syn::Ident | input.ident() |
syn::Generics | input.generics() |
syn::Visibility | input.vis() |
syn::DeriveInput | Full derive input |
syn::DataStruct / DataEnum / DataUnion | Specific derive data variant |
syn::Item | Full item |
syn::ItemFn / ItemStruct / etc. | Specific item variant |
Fields<T> | Struct fields |
Variants | Enum variants |
Data<T> | Derive data |
Extract<T: FromInput> | Delegates to T |
Attr<T: FromInput> | Delegates to T |
Threading Input Through zyn!
Inside zyn!, an input variable of type &zyn::Input is always in scope (defaults to a sentinel value). Shadow it before calling zyn! to pass real proc macro context:
#[proc_macro_derive(MyDerive)]
pub fn my_derive(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: zyn::Input = zyn::parse_input!(ts as syn::DeriveInput).into();
zyn::zyn! {
@my_element(name = some_ident)
}.into()
}
Every element’s render(&self, input: &Input) body also has input available directly — no need to pass it as a prop.
Advanced
Custom Names
By default the template name is derived from the function name. Pass a string to #[zyn::element] to override it:
#[zyn::element("say_hello")]
fn internal_greeting(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
zyn! { @say_hello(name = input.ident.clone()) }
The generated struct is still named SayHello (PascalCase of the custom name).
Custom names are useful when:
- The natural Rust function name is verbose or internal:
fn render_field_declaration→@field - You want a domain-specific vocabulary:
fn emit_getter_method→@getter - The function name conflicts with a Rust keyword:
fn type_decl→@type_def
#[zyn::element("getter")]
fn emit_getter_method(name: syn::Ident, ty: syn::Type) -> zyn::TokenStream {
zyn::zyn! {
pub fn {{ name | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
}
}
#[zyn::element("setter")]
fn emit_setter_method(name: syn::Ident, ty: syn::Type) -> zyn::TokenStream {
zyn::zyn! {
pub fn {{ name | ident:"set_{}" }}(&mut self, value: {{ ty }}) {
self.{{ name }} = value;
}
}
}
Namespaced Elements
Elements defined in submodules are referenced with :: path syntax:
mod components {
#[zyn::element]
pub fn field_decl(name: syn::Ident, ty: syn::Type) -> zyn::TokenStream {
zyn::zyn!({{ name }}: {{ ty }},)
}
}
zyn! {
@components::field_decl(
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
Only the last path segment is PascalCased — components::field_decl resolves to components::FieldDecl.
Organizing a Component Library
mod impls {
#[zyn::element]
pub fn display(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn! {
impl ::std::fmt::Display for {{ name }} {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "{}", self.name)
}
}
}
}
}
mod fields {
#[zyn::element]
pub fn required(name: syn::Ident, ty: syn::Type) -> zyn::TokenStream {
zyn::zyn!(pub {{ name }}: {{ ty }},)
}
#[zyn::element]
pub fn optional(name: syn::Ident, ty: syn::Type) -> zyn::TokenStream {
zyn::zyn!(pub {{ name }}: Option<{{ ty }}>,)
}
}
zyn! {
pub struct {{ name }} {
@for (field in fields.iter()) {
@if (field.is_optional) {
@fields::optional(name = field.ident.clone().unwrap(), ty = field.ty.clone())
} @else {
@fields::required(name = field.ident.clone().unwrap(), ty = field.ty.clone())
}
}
}
@impls::display(name = name.clone())
}
Paths can be as deep as needed — @a::b::c::my_element(...) resolves to a::b::c::MyElement.
Elements in Loops
Elements compose naturally with @for:
zyn! {
impl {{ struct_name }} {
@for (field in fields.iter()) {
@getter(
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
@setter(
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
}
Combining with @if
zyn! {
@for (field in fields.iter()) {
@if (field.attrs.has_attr("skip")) {}
@else {
@field_decl(
vis = field.vis.clone(),
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
}
Children Blocks in Loops
zyn! {
@for (variant in variants.iter()) {
@arm(pattern = variant.pat.clone()) {
Self::{{ variant.name }} => {{ variant.index }},
}
}
}
derive(Attribute)
#[derive(zyn::Attribute)] generates typed extraction for proc macro attributes like #[my_attr(skip, rename = "foo")]. Declare the shape as a Rust struct and zyn handles parsing.
Overview
use zyn::FromInput;
#[derive(zyn::Attribute)]
#[zyn("builder", about = "Configure the builder derive")]
struct BuilderConfig {
#[zyn(default = "build".to_string())]
method: String,
skip: bool,
rename: Option<String>,
}
let input: zyn::Input = derive_input.into();
let cfg = BuilderConfig::from_input(&input)?;
Attribute Mode
Add #[zyn("name")] to activate attribute mode. The derive searches input.attrs() for a matching #[name(...)] attribute, parses its arguments, and constructs your struct:
#[derive(zyn::Attribute)]
#[zyn("serde", unique, about = "Configure serialization")]
struct SerdeConfig {
#[zyn(0, about = "the input path")]
path: String,
#[zyn("rename_all", about = "case transform for keys")]
casing: Option<String>,
#[zyn(about = "reject unknown fields")]
deny_unknown_fields: bool,
#[zyn(default = "json".to_string(), about = "output format")]
format: String,
}
Generated methods:
from_input(input: &Input) -> zyn::Result<Self>— implementsFromInput; accumulates all errorsabout() -> &'static str— human-readable description
Error Accumulation
from_args collects all validation errors and returns them together as Diagnostics:
- Missing fields, type mismatches, and unknown keys are all reported at once
- Close misspellings suggest the correct field name via Levenshtein distance
abouttext is included in error messages as context
Argument Mode
Without #[zyn("name")], the derive generates only from_args and FromArg. Used as nested field types inside attribute mode structs:
#[derive(zyn::Attribute)]
struct Inner {
a: i64,
b: String,
}
#[derive(zyn::Attribute)]
#[zyn("outer")]
struct Outer {
inner: Inner, // parsed from: outer(inner(a = 1, b = "x"))
}
Struct-level Annotations
| Annotation | Effect |
|---|---|
#[zyn("name")] | Attribute name to match — activates attribute mode |
#[zyn(about = "...")] | Description for about() and error messages |
#[zyn(unique)] | Only one occurrence allowed; multiple → error |
Combinable: #[zyn("serde", unique, about = "Configure serialization")]
Field Annotations
| Annotation | Effect |
|---|---|
#[zyn(0)] | Positional: consume args[0] |
#[zyn("key")] | Name override: look up args.get("key") |
| (bare field) | Uses the field’s own name |
#[zyn(default)] | Use Default::default() when absent |
#[zyn(default = expr)] | Use expression as default (wrapped in Into::into) |
#[zyn(skip)] | Always Default::default(), never extracted |
#[zyn(about = "...")] | Description for error messages and about() |
Required vs Optional
- Fields without
#[zyn(default)]or#[zyn(skip)]and notOption<T>→ required Option<T>→ always optional (absent →None)bool→ always optional (absent →false)#[zyn(default)]→ useDefault::default()when absent#[zyn(default = expr)]→ use expression when absent
Enum Variants
Enums derive in argument mode — they generate from_arg and FromArg:
#[derive(zyn::Attribute)]
enum Mode {
Fast,
Slow,
Custom { speed: i64 },
}
#[derive(zyn::Attribute)]
#[zyn("config")]
struct Config {
mode: Mode, // matches: fast | slow | custom(speed = 5)
}
Variant dispatch by snake_case name:
- Unit variants —
fast→Mode::Fast - Struct variants —
custom(speed = 5)→Mode::Custom { speed: 5 } - Single-field tuple —
name = "blue"→Color::Named("blue".into()) - Multi-field tuple —
rgb(255, 0, 0)→Color::Rgb(255, 0, 0)
Using with Elements
Attribute mode structs implement FromInput and can be used as extractors:
#[zyn::element]
fn my_element(
#[zyn(input)] cfg: zyn::Attr<MyConfig>,
name: syn::Ident,
) -> zyn::TokenStream {
zyn::zyn! { /* cfg.format, cfg.enabled, name all available */ }
}
Proc Macro Entry Points
zyn provides attribute macros that replace #[proc_macro_derive] and #[proc_macro_attribute] with a declarative, template-first workflow.
#[zyn::derive]— replaces#[proc_macro_derive]#[zyn::attribute]— replaces#[proc_macro_attribute]
What’s Generated
Both macros generate a complete proc macro entry point:
- Input parsing —
DeriveInputfor derives,Itemfor attributes, wrapped inzyn::Input inputbinding — available in scope forzyn!and element extractors- Extractor resolution —
#[zyn(input)]parameters are resolved viaFromInput - Diagnostic macros —
error!,warn!,note!,help!,bail!(same as#[zyn::element]) - Return type conversion — your
proc_macro2::TokenStreamis automatically converted toproc_macro::TokenStream
You write a function that returns zyn::TokenStream. The macro handles everything else.
#[zyn::derive]
Replaces #[proc_macro_derive(...)]. The annotated item is parsed as syn::DeriveInput and wrapped in Input::Derive. All parameters must be #[zyn(input)] extractors — derive macros don’t receive arguments.
Basic Derive
The derive name is the PascalCase of the function name:
#[zyn::derive]
fn my_builder(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
) -> zyn::TokenStream {
zyn::zyn!(
impl {{ ident }} {
pub fn build(self) -> Self { self }
}
)
}
The function name my_builder becomes MyBuilder. Users apply it to a struct:
#[derive(MyBuilder)]
struct Config {
host: String,
port: u16,
}
// Generated:
// impl Config {
// pub fn build(self) -> Self { self }
// }
Multiple Extractors
Extract as many input properties as you need:
#[zyn::derive]
fn my_builder(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields,
#[zyn(input)] generics: zyn::Extract<zyn::syn::Generics>,
) -> zyn::TokenStream {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
zyn::zyn!(
impl {{ impl_generics }} {{ ident }} {{ ty_generics }} {{ where_clause }} {
@for (field in fields.iter()) {
pub fn {{ field.ident.as_ref().unwrap() | snake }}(
mut self,
value: {{ field.ty.clone() }},
) -> Self {
self.{{ field.ident.as_ref().unwrap() }} = value;
self
}
}
}
)
}
Usage with generics:
#[derive(MyBuilder)]
struct Config<T: Default> {
host: String,
value: T,
}
// Generated:
// impl<T: Default> Config<T> {
// pub fn host(mut self, value: String) -> Self { self.host = value; self }
// pub fn value(mut self, value: T) -> Self { self.value = value; self }
// }
Custom Name
Override the derive name with a string argument:
#[zyn::derive("Builder")]
fn internal_builder_impl(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields,
) -> zyn::TokenStream {
zyn::zyn!(
impl {{ ident }} {
@for (field in fields.iter()) {
pub fn {{ field.ident.as_ref().unwrap() | snake }}(
mut self,
value: {{ field.ty.clone() }},
) -> Self {
self.{{ field.ident.as_ref().unwrap() }} = value;
self
}
}
}
)
}
Users write #[derive(Builder)] — the function name doesn’t matter:
#[derive(Builder)]
struct Config {
host: String,
port: u16,
}
Helper Attributes
Declare helper attributes so users can annotate fields:
#[zyn::derive("Builder", attributes(builder))]
fn builder_derive(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields,
) -> zyn::TokenStream {
zyn::zyn!(
impl {{ ident }} {
@for (field in fields.iter()) {
pub fn {{ field.ident.as_ref().unwrap() | snake }}(
mut self,
value: {{ field.ty.clone() }},
) -> Self {
self.{{ field.ident.as_ref().unwrap() }} = value;
self
}
}
}
)
}
Users can now write:
#[derive(Builder)]
struct Config {
#[builder(default = "8080")]
port: u16,
host: String,
}
Multiple helpers are comma-separated: attributes(builder, validate).
With Attribute Parsing
Combine helper attributes with #[derive(Attribute)] for typed extraction:
#[derive(zyn::Attribute)]
#[zyn("builder")]
struct BuilderConfig {
#[zyn(default)]
skip: bool,
}
#[zyn::derive("Builder", attributes(builder))]
fn builder_derive(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields,
#[zyn(input)] cfg: zyn::Attr<BuilderConfig>,
) -> zyn::TokenStream {
if cfg.skip {
return zyn::zyn!();
}
zyn::zyn!(
impl {{ ident }} {
@for (field in fields.iter()) {
pub fn {{ field.ident.as_ref().unwrap() | snake }}(
mut self,
value: {{ field.ty.clone() }},
) -> Self {
self.{{ field.ident.as_ref().unwrap() }} = value;
self
}
}
}
)
}
Usage:
#[derive(Builder)]
struct Config {
#[builder(skip)]
internal: bool,
host: String,
}
Using Elements
Elements work inside derive bodies because input is in scope:
#[zyn::element]
fn setter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
zyn::zyn!(
pub fn {{ name | snake }}(mut self, value: {{ ty }}) -> Self {
self.{{ name }} = value;
self
}
)
}
#[zyn::derive]
fn my_builder(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields,
) -> zyn::TokenStream {
zyn::zyn!(
impl {{ ident }} {
@for (field in fields.iter()) {
@setter(
name = field.ident.clone().unwrap(),
ty = field.ty.clone(),
)
}
}
)
}
Usage is identical — the element is an implementation detail:
#[derive(MyBuilder)]
struct User {
name: String,
age: u32,
}
// Generated:
// impl User {
// pub fn name(mut self, value: String) -> Self { self.name = value; self }
// pub fn age(mut self, value: u32) -> Self { self.age = value; self }
// }
Diagnostics
All diagnostic macros are available — error!, warn!, note!, help!, bail!:
#[zyn::derive]
fn my_derive(
#[zyn(input)] fields: zyn::Fields,
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
) -> zyn::TokenStream {
if fields.is_empty() {
bail!("at least one field is required");
}
for field in fields.iter() {
if field.ident.is_none() {
error!("tuple structs are not supported"; span = field.span());
}
}
bail!();
warn!("this derive is experimental");
zyn::zyn!(impl {{ ident }} {})
}
bail!() with no arguments returns early only if errors have been pushed. bail!("msg") pushes an error and returns immediately. See Diagnostics for full details.
#[zyn::attribute]
Replaces #[proc_macro_attribute]. The annotated item is parsed as syn::Item and wrapped in Input::Item. Non-extractor parameters (at most one) receive the attribute arguments.
With Arguments and Extractors
The most common pattern — extract data from the annotated item and receive attribute arguments. The args parameter can be any type that implements syn::parse::Parse:
use zyn::syn::punctuated::Punctuated;
#[zyn::attribute]
fn trace_var(
#[zyn(input)] item: zyn::syn::ItemFn,
args: Punctuated<zyn::syn::Ident, zyn::syn::Token![,]>,
) -> zyn::TokenStream {
let vars: std::collections::HashSet<zyn::syn::Ident> = args.into_iter().collect();
// transform the function using vars...
zyn::zyn!({ { item } })
}
Usage — item is the annotated function, args contains x and y:
#[trace_var(x, y)]
fn factorial(mut n: u64) -> u64 {
let mut p = 1u64;
while n > 1 {
p *= n;
n -= 1;
}
p
}
// At runtime, assignments to x and y print their values
Without Arguments
When your attribute doesn’t accept arguments, use only extractors:
#[zyn::attribute]
fn instrument(
#[zyn(input)] item: zyn::syn::ItemFn,
) -> zyn::TokenStream {
let name = &item.sig.ident;
zyn::zyn!(
fn {{ name }}() {
::std::println!("entering {}", ::std::stringify!({{ name }}));
let __result = (|| {{ item.block.clone() }})();
::std::println!("exiting {}", ::std::stringify!({{ name }}));
__result
}
)
}
Usage — no arguments needed:
#[instrument]
fn compute() {
// ...
}
// At runtime:
// entering compute
// exiting compute
Without Extractors
When you only need the arguments, not the input item — pass the item through unchanged:
#[zyn::attribute]
fn deprecated_alias(args: zyn::syn::LitStr) -> zyn::TokenStream {
let _alias = args.value();
zyn::zyn!()
}
Usage:
#[deprecated_alias(name = "old_name")]
fn new_name() { /* ... */ }
No #[zyn(input)] parameters means the item is still parsed into input but nothing extracts from it.
With Typed Arguments
Any type that implements syn::parse::Parse works as the args parameter:
#[zyn::attribute]
fn rename(
#[zyn(input)] item: zyn::syn::ItemFn,
args: zyn::syn::LitStr,
) -> zyn::TokenStream {
let new_name = zyn::format_ident!("{}", args.value());
zyn::zyn!(
fn {{ new_name }}() {{ item.block.clone() }}
)
}
Usage — the args are parsed as a LitStr directly:
#[rename("calculate")]
fn compute() -> i32 { 42 }
// Generated:
// fn calculate() -> i32 { 42 }
With Struct Extractors
Extract specific item types — the FromInput impl handles validation:
#[zyn::attribute]
fn add_debug(
#[zyn(input)] item: zyn::syn::ItemStruct,
) -> zyn::TokenStream {
let name = &item.ident;
zyn::zyn!(
{{ item }}
impl ::std::fmt::Debug for {{ name }} {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.debug_struct(::std::stringify!({{ name }})).finish()
}
}
)
}
Usage:
#[add_debug]
struct Point {
x: f32,
y: f32,
}
// Generated:
// struct Point { x: f32, y: f32 }
// impl Debug for Point {
// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// f.debug_struct("Point").finish()
// }
// }
If #[add_debug] is applied to a non-struct (e.g., a function), the ItemStruct extractor produces a compile error automatically.
Using Elements
Elements work inside attribute bodies because input is in scope:
#[zyn::element]
fn debug_field(name: zyn::syn::Ident) -> zyn::TokenStream {
zyn::zyn!(
.field(::std::stringify!({{ name }}), &self.{{ name }})
)
}
#[zyn::attribute]
fn auto_debug(
#[zyn(input)] item: zyn::syn::ItemStruct,
#[zyn(input)] fields: zyn::Fields,
) -> zyn::TokenStream {
let name = &item.ident;
zyn::zyn!(
{{ item }}
impl ::std::fmt::Debug for {{ name }} {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.debug_struct(::std::stringify!({{ name }}))
@for (field in fields.iter()) {
@debug_field(name = field.ident.clone().unwrap())
}
.finish()
}
}
)
}
Usage:
#[auto_debug]
struct User {
name: String,
age: u32,
}
// Generated:
// struct User { name: String, age: u32 }
// impl Debug for User {
// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// f.debug_struct("User")
// .field("name", &self.name)
// .field("age", &self.age)
// .finish()
// }
// }
Diagnostics
All diagnostic macros are available — error!, warn!, note!, help!, bail!:
#[zyn::attribute]
fn must_be_pub(
#[zyn(input)] item: zyn::syn::ItemFn,
) -> zyn::TokenStream {
if !matches!(item.vis, zyn::syn::Visibility::Public(_)) {
bail!("function must be public"; span = item.sig.ident.span());
}
warn!("this attribute is experimental");
zyn::zyn!({ { item } })
}
Usage — applying to a non-public function produces a compile error:
#[must_be_pub]
fn private_fn() {}
// error: function must be public
// --> src/lib.rs:2:4
#[must_be_pub]
pub fn public_fn() {}
// compiles fine (with a warning: "this attribute is experimental")
See Diagnostics for full details on error!, warn!, note!, help!, and bail!.
Reference
Precise specifications for the zyn template language: formal grammar, parsing rules, expansion model, trait signatures, and error catalog.
These pages complement the tutorial chapters — reach for them when you need exact definitions rather than examples.
Grammar
Formal EBNF grammar for the zyn template language. zyn!(...) accepts a Template as input.
Template = Node*
Node = TokensNode
| InterpNode
| GroupNode
| AtNode
TokensNode = token_tree+
(* any token tree(s) that are NOT: '@', '{', '(', '[' *)
InterpNode = '{' '{' Expr ('|' Pipe)* '}' '}'
(* outer brace content must be exactly one inner brace group *)
Expr = token_tree+
(* all token trees before the first '|' or the closing inner '}' *)
Pipe = ident (':' PipeArg)*
PipeArg = token_tree+
(* tokens until the next ':' or '|' *)
GroupNode = '(' Template ')'
| '[' Template ']'
| '{' Template '}' (* only when brace disambiguation fails *)
AtNode = '@' 'if' IfBody
| '@' 'for' ForBody
| '@' 'match' MatchBody
| '@' ElementName ElementBody
IfBody = '(' Expr ')' '{' Template '}' ElseClause*
ElseClause = '@' 'else' 'if' '(' Expr ')' '{' Template '}'
| '@' 'else' '{' Template '}'
ForBody = '(' ident 'in' Expr ')' '{' Template '}'
| '(' Expr ')' '{' Template '}'
(* classic form: @for (count) { body } expands to for _ in 0..count *)
MatchBody = '(' Expr ')' '{' MatchArm* '}'
MatchArm = Pattern '=>' '{' Template '}' ','?
Pattern = token_tree+ (* all tokens before '=>' *)
ElementName = ident ('::' ident)*
ElementBody = ('(' Props ')')? ('{' Template '}')?
Props = PropField (',' PropField)* ','?
PropField = ident '=' PropValue
PropValue = token_tree+ (* all tokens until ',' or ')' *)
Parsing Model
Template::parse is the root parser. It reads token trees in a loop, dispatching on the leading token:
| Leading token | Action |
|---|---|
@ | Flush pending tokens; parse AtNode |
{ | Flush pending tokens; run brace disambiguation |
( | Flush pending tokens; parse GroupNode (Parenthesis) |
[ | Flush pending tokens; parse GroupNode (Bracket) |
| anything else | Accumulate into pending TokenStream |
When the input is exhausted, any pending tokens are flushed as a TokensNode.
Brace Disambiguation
When the parser sees {, it must decide between an InterpNode and a brace-delimited GroupNode:
- Consume the outer
{, capturing its content stream. - Fork the content stream and attempt to consume a single inner
{group. - If the fork consumed exactly one inner brace group AND the outer content is now empty → interpolation (
InterpNode). - Otherwise → group (
GroupNodewithDelimiter::Brace).
In concrete terms:
{{ expr }}— outer brace contains only{ expr }→InterpNode{ anything }where content is not a single inner brace group →GroupNode{{ }}(empty inner) → parse error:"empty interpolation"
The {{ and }} are two separate { / } tokens, not a special lexeme.
Trait Reference
Render
pub trait Render {
fn render(&self, input: &Input) -> zyn::TokenStream;
}
Implemented by types generated by #[zyn::element]. Called inside zyn!(...) for every @element invocation. The input parameter carries the proc macro’s Input context — attributes, ident, generics, visibility of the annotated item. input is provided automatically by #[zyn::derive], #[zyn::attribute], and #[zyn::element]. For manual usage, define let input: Input = ...; before calling zyn!.
Elements are always infallible — diagnostics are expressed via error!, warn!, note!, help!, and bail! macros generated by #[zyn::element], available inside element function bodies.
FromInput
pub trait FromInput: Sized {
fn from_input(input: &Input) -> zyn::Result<Self>;
}
Extracts typed data from an Input context. Implemented by:
#[derive(Attribute)]structs (attribute mode) — searchesinput.attrs()for a named attributesyn::Ident— returnsinput.ident()syn::Generics— returnsinput.generics()syn::Visibility— returnsinput.vis()Fields<T>— extracts struct fieldsVariants— extracts enum variantsData<T: FromData>— re-parses the full input asTExtract<T: FromInput>andAttr<T: FromInput>— wrapper delegates toT
FromFields
pub trait FromFields: Sized {
fn from_fields(fields: syn::Fields) -> zyn::Result<Self>;
}
Converts syn::Fields into a specific shape. Implemented for syn::Fields (identity), syn::FieldsNamed (errors on non-named), syn::FieldsUnnamed (errors on non-unnamed). Used as the type parameter of Fields<T>.
Pipe
pub trait Pipe {
type Input;
type Output: quote::ToTokens;
fn pipe(&self, input: Self::Input) -> Self::Output;
}
Implemented by types generated by #[zyn::pipe] and by all built-in pipe structs. Output must implement ToTokens so the final value in a pipe chain can be emitted to the token stream.
All built-in pipes accept String as Input. Custom pipes in a chain also receive String because the intermediate value is re-stringified between each pipe via .to_string().
Expand (internal)
pub trait Expand {
fn expand(
&self,
output: &syn::Ident,
idents: &mut ident::Iter,
) -> zyn::TokenStream;
}
Internal trait implemented by every AST node type. output is the name of the current accumulator variable (__zyn_ts_N). idents is a shared counter used to allocate unique variable names for nested structures. Not part of the public API; relevant only when implementing new AST node types.
Error Reference
Parse Errors
Errors produced during zyn!(...) expansion:
| Situation | Error |
|---|---|
{{ }} (empty interpolation) | "empty interpolation" |
@else not preceded by @if | "unexpected @else without @if" |
@for (x foo ...) — wrong keyword between binding and iter | "expected 'in'" |
@element(prop value) — missing = in prop | syn parse error |
| Unrecognized token where expression expected | syn parse error |
Generated Code Errors
| Situation | Error |
|---|---|
Pipe Output type not implementing ToTokens | Rust type error in generated code |
| Element struct field type mismatch at call site | Rust type error in generated code |
Attribute Extraction Errors
#[derive(Attribute)] accumulates all validation errors via the Diagnostics type (zyn::Result<T> = Result<T, Diagnostics>):
| Situation | Error |
|---|---|
| Missing required field | "missing required field \name`“withabout` text if available |
| Type mismatch | "expected string literal" etc. from FromArg |
| Unknown named argument | "unknown argument \naem`“with“did you mean `name`?”` if a close match exists (Levenshtein distance ≤ 3) |
All errors are collected and returned together as a single Diagnostics value.
Element Diagnostic Macros
#[zyn::element] generates local error!, warn!, note!, help!, and bail! macros that push diagnostics to the element’s diagnostics accumulator. bail! returns early if errors exist. All accept format!-style arguments and an optional ; span = expr suffix. These macros are only available inside #[zyn::element] bodies.
See Diagnostics for usage examples.
Debugging with debug!
zyn::debug! is a drop-in replacement for zyn! that prints what the template produces, then returns the same tokens. Use it to see exactly what code your template generates.
Basic Usage
let tokens = zyn::debug! {
struct {{ name }} {
@for (field in fields.iter()) {
{{ field.ident }}: {{ field.ty }},
}
}
};
zyn::debug! ─── pretty
struct MyStruct {
name: String,
age: u32,
}
Modes
Specify a mode before => to control the output format:
pretty (default)
Shows the final Rust code your template produces — the actual output, formatted with indentation.
zyn::debug! { pretty =>
@if (is_pub) { pub }
fn {{ name | snake }}() {}
}
When no mode is specified, pretty is used:
zyn::debug! {
fn {{ name }}() {}
}
The output appears on stderr at runtime (when the proc macro executes), so it’s visible in cargo build and cargo test output.
raw
Shows the expansion code — the token-building machinery that zyn! generates behind the scenes. Emitted as a compile-time diagnostic (zero runtime cost).
zyn::debug! { raw =>
struct {{ name }} {}
}
note: zyn::debug! ─── raw
{
let mut output = TokenStream::new();
output.extend(quote!(struct));
ToTokens::to_tokens(&(name), &mut output);
output.extend(quote!({}));
output
}
The output is cleaned up for readability — __zyn_ts_0 becomes output, fully-qualified paths are simplified.
ast
Shows the parsed template structure — which AST nodes the parser created. Emitted as a compile-time diagnostic.
zyn::debug! { ast =>
@if (is_pub) { pub }
struct {{ name }} {}
}
note: zyn::debug! ─── ast
Template [
At(If)
Tokens("struct")
Interp { ... }
Tokens("{}")
]
Migration from Raw quote/syn
Common quote, syn, and proc_macro2 patterns and their zyn equivalents. Use this table when converting existing proc macros to zyn templates.
Inside Templates (zyn!)
| Raw Pattern | Zyn Alternative |
|---|---|
quote! { fn #name() {} } | zyn! { fn {{ name }}() {} } |
format_ident!("get_{}", name) | {{ name | ident:"get_{}" }} |
format_ident!("{}_impl", name) | {{ name | ident:"{}_impl" }} |
name.to_string().to_uppercase() | {{ name | upper }} |
name.to_string().to_lowercase() | {{ name | lower }} |
to_snake_case(name) | {{ name | snake }} |
to_pascal_case(name) | {{ name | pascal }} |
to_camel_case(name) | {{ name | camel }} |
LitStr::new(&name, span) | {{ name | str }} |
if cond { quote!(...) } else { quote!() } | @if (cond) { ... } @else { ... } |
items.iter().map(|i| quote!(...)).collect() + #(#tokens)* | @for (item in items) { ... } |
match kind { A => quote!(...), _ => quote!() } | @match (kind) { A => { ... } _ => {} } |
syn::Error::new(...).to_compile_error() | error!("msg"); bail!(); inside #[zyn::element] |
Manual #[deprecated] trick for warnings | warn!("msg") inside #[zyn::element] |
Reusable fn render(...) -> TokenStream | #[zyn::element] |
Reusable fn transform(input: String) -> Ident | #[zyn::pipe] |
No Zyn Alternative (use raw APIs)
These patterns have no template-level equivalent but are available through zyn’s public API:
| Pattern | zyn equivalent |
|---|---|
syn::parse_macro_input!(input as DeriveInput) | zyn::parse_input!(input as DeriveInput) |
syn::parse_str::<T>("...") | zyn::parse!("..." => T) |
syn::parse2::<T>(tokens) | zyn::parse!(tokens => T) |
quote::format_ident!("name") | zyn::format_ident!("name") |
Span::call_site() | zyn::Span::call_site() |
syn::fold::Fold / syn::visit::Visit | syn::fold::Fold |
In book examples and tests,
zyn::format_ident!()appears frequently because it constructs illustrative input values. In real proc macros, these values typically come from parsed input (input.ident,field.ty, etc.) rather than being constructed manually.