Introduction
Zyn is a proc macro framework with templates, composable elements, and built-in diagnostics.
zyn! {
@for (field in fields.iter()) {
pub fn {{ field.ident | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
Note
Templates are fully type-checked at compile time — errors appear inline, just like regular Rust code.

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 | debug / debug(pretty) + ZYN_DEBUG env var — inspect generated code as compiler diagnostics |
| 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 —
debug/debug(pretty)withZYN_DEBUGenv var - 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.4"
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!, #[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 | Extension traits (AttrExt, MetaExt, FieldExt, FieldsExt, TypeExt, DataExt, ItemExt, VariantExt) for working with syn AST types |
pretty | no | Pretty-printed debug output via prettyplease — use debug(pretty) on any macro attribute |
diagnostics | no | Rich compiler diagnostics via proc-macro2-diagnostics — errors, warnings, notes, and help messages with proper spans |
To enable optional features:
[dependencies]
zyn = { version = "0.4", features = ["ext", "pretty", "diagnostics"] }
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 —
debugattribute arg withZYN_DEBUGenv var
Templates
The zyn! macro is the core of zyn. It accepts a template and returns a zyn::Output:
let output: zyn::Output = 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 returns zyn::TokenStream; the macro wraps it in Output automatically:
#[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::Output {
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::mark::Builder 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());
}
}
bail!();
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
abouttext is included in error messages as context
Typo suggestions — When an unknown argument is close to a known field name, the error includes a “did you mean?” hint:
#[derive(zyn::Attribute)]
#[zyn("config")]
struct Config {
enabled: bool,
format: String,
}
// User writes #[config(enabed, fromat = "json")]
// Compiler output:
//
// error: unknown argument `enabed`
// --> src/lib.rs:10:10
// |
// 10 | #[config(enabed, fromat = "json")]
// | ^^^^^^
// |
// = help: did you mean `enabled`?
//
// error: unknown argument `fromat`
// --> src/lib.rs:10:18
// |
// 10 | #[config(enabed, fromat = "json")]
// | ^^^^^^
// |
// = help: did you mean `format`?
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!.
Testing
zyn provides two categories of testing tools:
- Assertions — macros for comparing token streams, checking substrings, and inspecting diagnostics produced by elements
- Debugging — inspect generated code at compile time with
debug/debug(pretty)and theZYN_DEBUGenvironment variable
Assertions
The zyn! macro returns [Output], which carries both generated tokens and any diagnostics from element renders. zyn provides assertion macros for testing both.
Setup
Element tests require an input variable in scope:
let input: zyn::Input = zyn::parse!("struct Test;" => zyn::syn::DeriveInput)
.unwrap()
.into();
Comparing tokens
Use assert_tokens! to compare the full output against an expected quote! expression:
#[test]
fn generates_getter() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let name = zyn::format_ident!("get_name");
let output = zyn::zyn!(
fn {{ name }}(&self) -> &str {
&self.name
}
);
let expected = zyn::quote::quote!(
fn get_name(&self) -> &str {
&self.name
}
);
zyn::assert_tokens!(output, expected);
}
For partial matching when you only care about a fragment:
let output = zyn::zyn!(
struct Foo {
x: u32,
y: u32,
}
);
zyn::assert_tokens_contain!(output, "struct Foo");
// ✓ passes — "struct Foo" appears in the raw token output
To verify that an element produced no output (e.g., after bail!):
zyn::assert_tokens_empty!(output);
Asserting diagnostics
When an element emits errors via error! or bail!, the diagnostics are carried in the Output. Assert on them by level and message substring:
#[zyn::element]
fn validated(name: zyn::syn::Ident) -> zyn::TokenStream {
if name == "forbidden" {
bail!("reserved identifier `{}`", name);
}
zyn::zyn!(fn {{ name }}() {})
}
#[test]
fn rejects_forbidden_name() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let output = zyn::zyn!(@validated(name = zyn::format_ident!("forbidden")));
zyn::assert_diagnostic_error!(output, "reserved identifier");
zyn::assert_tokens_empty!(output);
// ✓ error diagnostic with "reserved identifier" message
// ✓ no tokens produced (bail! stopped execution)
}
#[test]
fn accepts_valid_name() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let output = zyn::zyn!(@validated(name = zyn::format_ident!("hello")));
zyn::assert_tokens_contain!(output, "fn hello");
// ✓ tokens contain "fn hello"
}
Warnings don’t block output
Non-error diagnostics (warn!, note!, help!) are accumulated but don’t suppress token generation. Both tokens and diagnostics are present in the Output:
#[zyn::element]
fn deprecated_getter(name: zyn::syn::Ident) -> zyn::TokenStream {
warn!("this getter pattern is deprecated");
zyn::zyn!(fn {{ name }}(&self) -> &str { &self.name })
}
#[test]
fn warning_preserved_alongside_output() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let output = zyn::zyn!(@deprecated_getter(name = zyn::format_ident!("get_name")));
zyn::assert_tokens_contain!(output, "fn get_name");
zyn::assert_diagnostic_warning!(output, "deprecated");
// ✓ tokens produced despite warning
// ✓ warning diagnostic preserved in output
}
Multiple diagnostics
Elements can accumulate multiple diagnostics across different levels:
#[zyn::element]
fn multi_diag() -> zyn::TokenStream {
error!("missing field `x`");
error!("missing field `y`");
warn!("deprecated usage");
bail!();
zyn::TokenStream::new()
}
#[test]
fn accumulates_all_diagnostics() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let output = zyn::zyn!(@multi_diag());
zyn::assert_diagnostic_error!(output, "missing field `x`");
zyn::assert_diagnostic_error!(output, "missing field `y`");
zyn::assert_diagnostic_warning!(output, "deprecated usage");
// ✓ all three diagnostics present
// ✓ both errors and the warning accumulated before bail!
}
Pretty assertions
With the pretty feature enabled, use assert_tokens_pretty! and assert_tokens_contain_pretty! to compare against prettyplease-formatted output:
#[zyn::element]
fn impl_block(name: zyn::syn::Ident) -> zyn::TokenStream {
zyn::zyn!(
impl {{ name }} {
fn validate(&self) -> bool { true }
}
)
}
#[test]
fn pretty_token_comparison() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let output = zyn::zyn!(@impl_block(name = zyn::format_ident!("Foo")));
let expected = zyn::quote::quote!(
impl Foo {
fn validate(&self) -> bool { true }
}
);
zyn::assert_tokens_pretty!(output, expected);
// ✓ compared using prettyplease-formatted output
}
#[test]
fn pretty_substring_check() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput)
.unwrap()
.into();
let output = zyn::zyn!(@impl_block(name = zyn::format_ident!("Foo")));
zyn::assert_tokens_contain_pretty!(output, "fn validate");
// ✓ substring found in pretty-printed output
}
Macro reference
| Macro | Purpose |
|---|---|
assert_tokens!(actual, expected) | Compare two token streams (raw-formatted diff on failure) |
assert_tokens_empty!(output) | Assert no tokens produced |
assert_tokens_contain!(output, "substr") | Check for substring in raw token output |
assert_diagnostic!(output, Level, "msg") | Assert diagnostic at a specific level with message |
assert_diagnostic_error!(output, "msg") | Assert error diagnostic |
assert_diagnostic_warning!(output, "msg") | Assert warning diagnostic |
assert_diagnostic_note!(output, "msg") | Assert note diagnostic |
assert_diagnostic_help!(output, "msg") | Assert help diagnostic |
assert_compile_error!(output, "msg") | Alias for assert_diagnostic_error! |
With the pretty feature enabled:
| Macro | Purpose |
|---|---|
assert_tokens_pretty!(actual, expected) | Compare using prettyplease-formatted output |
assert_tokens_contain_pretty!(output, "substr") | Substring check on pretty-printed output |
Debugging
Inspect generated code by adding the debug argument to any zyn attribute macro. Debug output is emitted as a compiler note diagnostic, visible in both terminal and IDE.
Setup
Two conditions must be met for debug output:
- Add
debug(ordebug(pretty)) to the macro attribute - Set the
ZYN_DEBUGenvironment variable to match the generated type name
#[zyn::element(debug)]
fn greeting(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
ZYN_DEBUG="*" cargo build
Without ZYN_DEBUG set, the debug argument is inert — no output, no overhead. This makes it safe to leave in source code during development.
Note
An element annotated with
debug— the argument is inert untilZYN_DEBUGis set.

Supported macros
| Macro | Syntax |
|---|---|
#[zyn::element] | #[zyn::element(debug)], #[zyn::element("name", debug)] |
#[zyn::pipe] | #[zyn::pipe(debug)], #[zyn::pipe("name", debug)] |
#[zyn::derive] | #[zyn::derive("Name", debug)], #[zyn::derive("Name", attributes(skip), debug)] |
#[zyn::attribute] | #[zyn::attribute(debug)] |
Options
Options are passed as a comma-separated list inside parentheses:
| Syntax | Output | Format |
|---|---|---|
debug | Body only, {{ prop }} placeholders | Raw |
debug(pretty) | Body only, {{ prop }} placeholders | Pretty-printed |
debug(full) | Full struct + impl | Raw |
debug(pretty, full) | Full struct + impl | Pretty-printed |
debug(key = "val", ...) | Body only, matched props substituted | Raw |
debug(pretty, key = "val", ...) | Body only, matched props substituted | Pretty-printed |
Options can be combined in any order: debug(full, pretty) is equivalent to debug(pretty, full). Injection pairs can be mixed with pretty and full freely.
Static injection
Debug output runs at proc-macro time — runtime values don’t exist yet. When a prop like name or ty appears in a {{ name }} interpolation, the debug note shows {{ name }} as a placeholder. Static injection lets you supply a representative value so the output shows something meaningful.
Syntax
// No injection — placeholders in output
#[zyn::element(debug)]
// Inject a single prop
#[zyn::element(debug(name = "Foo"))]
// Inject multiple props
#[zyn::element(debug(name = "Foo", ty = "String"))]
// Combine with pretty
#[zyn::element(debug(pretty, name = "Foo", ty = "Vec<u8>"))]
All injection values are string literals. The string content is parsed as a proc_macro2::TokenStream — any valid Rust token sequence works: identifiers, types, expressions, literals.
Example
#[zyn::element(debug(pretty, name = "Foo", ty = "String"))]
fn setter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
zyn::zyn! {
fn {{ name }}(mut self, value: {{ ty }}) -> Self {
self.{{ name }} = Some(value);
self
}
}
}
ZYN_DEBUG="Setter" cargo build:
note: zyn::element ─── Setter
fn Foo(mut self, value: String) -> Self {
self.Foo = Some(value);
self
}
--> src/lib.rs:1:1
Without injection:
note: zyn::element ─── Setter
fn {{ name }}(mut self, value: {{ ty }}) -> Self {
self.{{ name }} = Some(value);
self
}
--> src/lib.rs:1:1
Note
Injected prop resolved at proc-macro time — the output shows the real value instead of a placeholder.

Note
Injection with a pipe transform applied —
name = "HelloWorld"piped throughsnakeproduceshello_world.

Uninjected props
Any prop without a matching injection key renders as {{ prop_name }}. This is intentional — it makes unresolved placeholders visually distinct from real tokens.
Output formats
Raw (default)
The default format emits the raw TokenStream::to_string() output. This is a flat, single-line string with fully-qualified paths and spaces between all tokens. No extra dependencies are required.
#[zyn::element(debug)]
fn greeting(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
ZYN_DEBUG="Greeting" cargo build
note: zyn::element ─── Greeting
fn {{ name }}() {}
--> src/lib.rs:1:1
The raw format is useful for quick checks and when you want to see the exact tokens being generated.
Note
Raw debug output shown as an inline compiler diagnostic in the editor.

Note
The same raw output surfaced in the Problems panel for easy navigation.

Note
Pretty-printed debug output in the console for a pipe.

Pretty (feature-gated)
The pretty option uses prettyplease to produce properly formatted Rust code with indentation and line breaks.
Enable the pretty feature in your Cargo.toml:
[dependencies]
zyn = { version = "0.4", features = ["pretty"] }
Then use debug(pretty):
#[zyn::element(debug(pretty))]
fn greeting(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
ZYN_DEBUG="Greeting" cargo build
note: zyn::element ─── Greeting
fn {{ name }}() {}
--> src/lib.rs:1:1
Note
Pretty-printed debug output — formatted with
prettypleasefor readable, indented Rust code.

If debug(pretty) is used without the pretty feature enabled, you’ll get a helpful compile error:
error: enable the `pretty` feature to use `debug(pretty)`
--> src/lib.rs:1:16
|
1 | #[zyn::element(debug(pretty))]
| ^^^^^^^^^^^^^^
Full output
Use debug(full) to emit the entire generated struct and impl instead of just the body:
#[zyn::element(debug(full))]
fn greeting(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
note: zyn::element ─── Greeting
struct Greeting { pub name : syn :: Ident , } impl ::zyn::Render for Greeting { fn render(&self, input : &::zyn::Input) -> ::zyn::Output { ... } }
--> src/lib.rs:1:1
Note
debug(full)shows the entire generated struct andimplblock as a raw inline diagnostic.

Combine with pretty for formatted full output:
#[zyn::element(debug(pretty, full))]
fn greeting(name: syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
Note
debug(pretty, full)formats the full struct + impl withprettypleasefor readable, indented output.

ZYN_DEBUG environment variable
The ZYN_DEBUG environment variable controls which items produce debug output. It accepts comma-separated patterns with * wildcards, matched against the generated type name (the PascalCase struct name, not the function name).
For an element defined as fn greeting(...), the generated type is Greeting. For a pipe fn shout(...), the type is Shout.
| Pattern | Matches |
|---|---|
* | Everything |
Greeting | Exact match only |
Greet* | Greeting, GreetingElement, etc. |
*Element | FieldElement, GreetingElement, etc. |
Greeting,Shout | Greeting and Shout |
Greet*,Shout,*Pipe | Mix wildcards and exact |
ZYN_DEBUG="*" cargo build
ZYN_DEBUG="Greeting" cargo build
ZYN_DEBUG="Greet*" cargo build
ZYN_DEBUG="Greeting,Shout" cargo build
Noise stripping
Before formatting (in both raw and pretty modes), zyn strips internal boilerplate from the generated code:
#[doc = "..."]attributes — removes the doc comment blocks on generated diagnostic macros#[allow(...)]attributes — removes#[allow(unused)]and similarmacro_rules!definitions — removes the internalerror!,warn!,note!,help!, andbail!macro definitions
This keeps the debug output focused on the code you care about.
Full example
Given this element:
#[zyn::element(debug(pretty))]
fn field_getter(
name: syn::Ident,
ty: syn::Type,
) -> zyn::TokenStream {
zyn::zyn!(
pub fn {{ name | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
)
}
Running with ZYN_DEBUG="FieldGetter" cargo build (no injection — props show as placeholders):
note: zyn::element ─── FieldGetter
pub fn {{ name | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
With static injection to see realistic output:
#[zyn::element(debug(pretty, name = "title", ty = "String"))]
fn field_getter(
name: syn::Ident,
ty: syn::Type,
) -> zyn::TokenStream {
zyn::zyn!(
pub fn {{ name | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
)
}
note: zyn::element ─── FieldGetter
pub fn get_title(&self) -> &String {
&self.title
}
Pipeline API
For library authors building on top of zyn, the debug module exposes a pipeline API via the DebugExt trait:
use zyn::debug::DebugExt;
// Raw format — always available
let raw: String = tokens.debug().raw();
// Pretty format — requires `pretty` feature
#[cfg(feature = "pretty")]
let pretty: String = tokens.debug().pretty();
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::Output;
}
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 toTsyn::DeriveInput— returns the full derive inputsyn::DataStruct,syn::DataEnum,syn::DataUnion— extracts derive data variantsyn::Itemand specific item types (syn::ItemFn,syn::ItemStruct, etc.) — extracts from item inputsyn::ItemStruct,syn::ItemEnum,syn::ItemUnion— also work with derive inputs via reconstruction
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>.
FromData
pub trait FromData: Sized {
fn from_data(data: syn::Data) -> zyn::Result<Self>;
}
Converts syn::Data into a specific data representation. Implemented for syn::Data (any kind), syn::DataStruct (errors on non-struct), syn::DataEnum (errors on non-enum), syn::DataUnion (errors on non-union). Used as the type parameter of Data<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 Diagnostic type (zyn::Result<T> = Result<T, Diagnostic>):
| 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 Diagnostic value.
Typo recovery: Unknown arguments within Levenshtein distance 3 of a known field automatically generate a
help: did you mean ...?suggestion pointing at the typo span.
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.
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.