Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

zyn

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
let vis = if is_pub {
    quote!(pub)
} else {
    quote!()
};

let fields_ts: Vec<_> = fields
    .iter()
    .map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        quote! { #name: #ty, }
    })
    .collect();

quote! {
    #vis struct #ident {
        #(#fields_ts)*
    }
}
zyn! {
    @if (is_pub) { pub }
    struct {{ ident }} {
        @for (field in fields.iter()) {
            {{ field.ident }}: {{ field.ty }},
        }
    }
}

2. Case conversion needs external crates

Renaming an identifier means importing heck, calling a conversion function, then wrapping it in format_ident!.

❌ Before✅ After
use heck::ToSnakeCase;

let getter = format_ident!(
    "get_{}",
    name.to_string().to_snake_case()
);
{{ name | snake | ident:"get_{}" }}

3. Diagnostics are fragmented

Errors, warnings, notes, and help messages each use a different mechanism — or aren’t possible at all.

❌ Before✅ After
// errors
return syn::Error::new_spanned(
    &input,
    "expected a struct",
).to_compile_error().into();

// warnings — needs proc-macro-error crate
emit_warning!(span, "deprecated");

// notes/help — not possible on stable
#[zyn::element]
fn validated(
    #[zyn(input)] ident: syn::Ident,
) -> zyn::TokenStream {
    error!("expected a struct";
        span = ident.span());
    note!("only named structs supported");
    help!("change input to a struct");
    bail!();

    warn!("this derive is deprecated");
    zyn::zyn! { /* ... */ }
}

4. Attribute parsing is reinvented every time

Every project writes its own parser for #[my_attr(skip, rename = "foo")].

❌ Before✅ After
for attr in &input.attrs {
    if attr.path().is_ident("my_attr") {
        let args = attr.parse_args_with(
            Punctuated::<Meta, Token![,]>
                ::parse_terminated
        )?;
        for meta in &args {
            match meta {
                Meta::Path(p)
                    if p.is_ident("skip") => {}
                Meta::NameValue(nv)
                    if nv.path.is_ident("rename") => {}
                _ => {}
            }
        }
    }
}
use zyn::ext::{AttrExt, AttrsExt};

let args = input.attrs
    .find_args("my_attr")?;

if args.has("skip") { /* ... */ }

if let Some(rename) = args.get("rename") {
    /* ... */
}

5. Reusable codegen means manual helper functions

There’s no composition model — just functions returning TokenStream.

❌ Before✅ After
fn render_field(
    vis: &Visibility,
    name: &Ident,
    ty: &Type,
) -> TokenStream {
    quote! { #vis #name: #ty, }
}

let tokens: Vec<_> = fields
    .iter()
    .map(|f| render_field(
        &f.vis,
        f.ident.as_ref().unwrap(),
        &f.ty,
    ))
    .collect();

quote! { struct #ident { #(#tokens)* } }
#[zyn::element]
fn field_decl(
    vis: syn::Visibility,
    name: syn::Ident,
    ty: syn::Type,
) -> zyn::TokenStream {
    zyn::zyn! { {{ vis }} {{ name }}: {{ ty }}, }
}

zyn! {
    struct {{ ident }} {
        @for (field in fields.iter()) {
            @field_decl(
                vis = field.vis.clone(),
                name = field.ident.clone().unwrap(),
                ty = field.ty.clone(),
            )
        }
    }
}

6. Five crates doing five things

❌ Before✅ After
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
heck = "0.5"
proc-macro-error = "1"
[dependencies]
zyn = "0.3"

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 }}
        }
    }
}
ConcernZyn approach
Code generationzyn! template with {{ }} interpolation — reads like the code it generates
Control flow@if, @for, @match directives inline — no .iter().map().collect()
Case conversionBuilt-in pipes: {{ name | snake }}, {{ name | pascal }}, {{ name | screaming }} — no extra crate
Name formatting{{ name | ident:"get_{}" }} — one expression, no let binding
Diagnosticserror!, 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
Debuggingzyn::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 any ToTokens value, with field access and method calls
  • Pipes{{ name | snake }}, {{ name | ident:"get_{}" }}, {{ name | str }} — 13 built-in pipes plus custom
  • Control flow@if, @for, @match with full nesting
  • Diagnosticserror!, 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
  • Debuggingzyn::debug! with pretty, raw, and ast modes
  • Attribute parsing#[derive(Attribute)] for typed attribute structs
  • Case conversionsnake, 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

FeatureDefaultDescription
deriveyesProc 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)]
extnoAttrExt 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

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

SyntaxPurpose
{{ expr }}Interpolation — insert any ToTokens value
{{ expr | pipe }}Pipes — transform values (case conversion, formatting)
@if / @for / @matchControl 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

PipeInputOutputExample
upperHelloWorldHELLOWORLD{{ name | upper }}
lowerHELLOhello{{ name | lower }}
snakeHelloWorldhello_world{{ name | snake }}
camelhello_worldhelloWorld{{ name | camel }}
pascalhello_worldHelloWorld{{ name | pascal }}
screamingHelloWorldHELLO_WORLD{{ name | screaming }}
kebabHelloWorld"hello-world"{{ name | kebab }}
strhello"hello"{{ name | str }}
trim__foo__foo{{ name | trim }}
pluralUserUsers{{ name | plural }}
singularusersuser{{ name | singular }}

Warning

kebab and str return 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_declFieldDecl.

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

TypeExtractsUse Case
Attr<T>T::from_input(input)#[derive(Attribute)] structs
Extract<T>T::from_input(input)General FromInput wrapper
Fields<T>Struct fieldsDefaults to syn::Fields
VariantsEnum variantsVec<syn::Variant>
Data<T>Derive dataDefaults 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:

TBehaviour
syn::Fields (default)Returns any field shape
syn::FieldsNamedErrors if not named fields
syn::FieldsUnnamedErrors 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:

TBehaviour
syn::Data (default)Returns any data kind
syn::DataStructErrors if not a struct
syn::DataEnumErrors if not an enum
syn::DataUnionErrors 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

TypeMatches
syn::DeriveInputAny derive input
syn::DataStructStruct data only
syn::DataEnumEnum data only
syn::DataUnionUnion data only

Item Input Types

TypeMatches
syn::ItemAny item
syn::ItemFnfn
syn::ItemStructstruct
syn::ItemEnumenum
syn::ItemUnionunion
syn::ItemTraittrait
syn::ItemImplimpl
syn::ItemTypetype
syn::ItemModmod
syn::ItemConstconst
syn::ItemStaticstatic
syn::ItemUseuse
syn::ItemExternCrateextern crate
syn::ItemForeignModextern "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:

TypeExtracts
#[derive(Attribute)] structsNamed attribute from input.attrs()
syn::Identinput.ident()
syn::Genericsinput.generics()
syn::Visibilityinput.vis()
syn::DeriveInputFull derive input
syn::DataStruct / DataEnum / DataUnionSpecific derive data variant
syn::ItemFull item
syn::ItemFn / ItemStruct / etc.Specific item variant
Fields<T>Struct fields
VariantsEnum 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> — implements FromInput; accumulates all errors
  • about() -> &'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
  • about text 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

AnnotationEffect
#[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

AnnotationEffect
#[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 not Option<T>required
  • Option<T> → always optional (absent → None)
  • bool → always optional (absent → false)
  • #[zyn(default)] → use Default::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 variantsfastMode::Fast
  • Struct variantscustom(speed = 5)Mode::Custom { speed: 5 }
  • Single-field tuplename = "blue"Color::Named("blue".into())
  • Multi-field tuplergb(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.

What’s Generated

Both macros generate a complete proc macro entry point:

  1. Input parsingDeriveInput for derives, Item for attributes, wrapped in zyn::Input
  2. input binding — available in scope for zyn! and element extractors
  3. Extractor resolution#[zyn(input)] parameters are resolved via FromInput
  4. Diagnostic macroserror!, warn!, note!, help!, bail! (same as #[zyn::element])
  5. Return type conversion — your proc_macro2::TokenStream is automatically converted to proc_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 tokenAction
@Flush pending tokens; parse AtNode
{Flush pending tokens; run brace disambiguation
(Flush pending tokens; parse GroupNode (Parenthesis)
[Flush pending tokens; parse GroupNode (Bracket)
anything elseAccumulate 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:

  1. Consume the outer {, capturing its content stream.
  2. Fork the content stream and attempt to consume a single inner { group.
  3. If the fork consumed exactly one inner brace group AND the outer content is now empty → interpolation (InterpNode).
  4. Otherwise → group (GroupNode with Delimiter::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) — searches input.attrs() for a named attribute
  • syn::Ident — returns input.ident()
  • syn::Generics — returns input.generics()
  • syn::Visibility — returns input.vis()
  • Fields<T> — extracts struct fields
  • Variants — extracts enum variants
  • Data<T: FromData> — re-parses the full input as T
  • Extract<T: FromInput> and Attr<T: FromInput> — wrapper delegates to T

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:

SituationError
{{ }} (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 propsyn parse error
Unrecognized token where expression expectedsyn parse error

Generated Code Errors

SituationError
Pipe Output type not implementing ToTokensRust type error in generated code
Element struct field type mismatch at call siteRust type error in generated code

Attribute Extraction Errors

#[derive(Attribute)] accumulates all validation errors via the Diagnostics type (zyn::Result<T> = Result<T, Diagnostics>):

SituationError
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 PatternZyn 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 warningswarn!("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:

Patternzyn 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::Visitsyn::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.