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

#[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!.