aacebo/zyn

A procedural macro development framework designed to simplify and add structure to rust macro development

Rust

196

219 commits

updated Sep 18, 2026

See the code

See what people are saying

SourceMessageScoreDate

Moxy - a Rust syntax toolkit designed to bring more cohesion to Rust parsing, tokenization, formatting, diagnostics, and quasi-quoting. (r/rust)

Last year I published [zyn](https://github.com/aacebo/zyn), a template engine for Rust proc macros. I got a lot of great constructive feedback from the community, which inspired me to take the ideas behind it a step further. `moxy` has been written from the ground up with a few goals in mind: # 1.…

34

Sep 26, 2026

README

Getting Started docs.rs crates.io bencher.dev Crates.io Size

A proc macro framework with templates, composable elements, and built-in diagnostics.

[!IMPORTANT] Zyn is being superseded by Moxy. Moxy takes the ideas behind Zyn further by owning the token, AST, parsing, templating, formatting, and diagnostics stack. Some Zyn features are not yet available in Moxy, but are planned to be added soon.

Read Moxy is replacing Zyn for more about the project and the rationale behind the change.

🗺️ Roadmap

cargo add zyn

Table of Contents


Templates

Templates are fully type-checked at compile time — errors appear inline, just like regular Rust code.

Compile-time type safety

The zyn! macro is the core of zyn. Write token output as if it were source code, with {{ }} interpolation and @ control flow directives.

Interpolation — any ToTokens value:

let name = zyn::format_ident!("hello_world");
zyn::zyn!(fn {{ name }}() {})
// → fn hello_world() {}

Pipes — transform values inline:

zyn::zyn!(fn {{ name | pascal }}() {})
// name = "hello_world" → fn HelloWorld() {}

Control flow:

zyn::zyn!(
    @if (is_pub) { pub }
    @for (field in fields.named.iter()) {
        fn {{ field.ident }}(&self) -> &{{ field.ty }} {
            &self.{{ field.ident }}
        }
    }
)

Full template syntax:

SyntaxPurpose
{{ expr }}Interpolate any ToTokens value
{{ expr | pipe }}Transform value through a pipe before inserting
@if (cond) { ... }Conditional token emission
@else { ... }Else branch
@else if (cond) { ... }Else-if branch
@for (x in iter) { ... }Loop over an iterator
@for (N) { ... }Repeat N times
@match (expr) { pat => { ... } }Pattern-based emission
@element_name(prop = val)Invoke a #[element] component

Elements

Elements are reusable template components defined with #[zyn::element]. They encapsulate a fragment of token output and accept typed props.

Define an element:

#[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 it inside any template with @:

zyn::zyn! {
    impl {{ ident }} {
        @for (field in fields.named.iter()) {
            @getter(name = field.ident.clone().unwrap(), ty = field.ty.clone())
        }
    }
}

Elements can also receive extractors — values resolved automatically from proc macro input — by marking a param with #[zyn(input)]:

#[zyn::derive]
fn my_getters(
    #[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
    #[zyn(input)] fields: zyn::Fields<zyn::syn::FieldsNamed>,
) -> zyn::TokenStream {
    zyn::zyn! {
        impl {{ ident }} {
            @for (field in fields.named.iter()) {
                pub fn {{ field.ident | snake | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
                    &self.{{ field.ident }}
                }
            }
        }
    }
}
// Applied to: struct User { first_name: String, age: u32 }
// Generates:
// impl User {
//     pub fn get_first_name(&self) -> &String { &self.first_name }
//     pub fn get_age(&self) -> &u32 { &self.age }
// }

Pipes

Pipes transform interpolated values: {{ expr | pipe }}. They chain left to right:

zyn::zyn!(fn {{ name | snake | ident:"get_{}" }}() {})
// name = "HelloWorld" → fn get_hello_world() {}

Built-in pipes:

PipeInput exampleOutput
snakeHelloWorldhello_world
pascalhello_worldHelloWorld
camelhello_worldhelloWorld
screamingHelloWorldHELLO_WORLD
kebabHelloWorld"hello-world"
upperhelloHELLO
lowerHELLOhello
strhello"hello"
trim__foo__foo
pluraluserusers
singularusersuser
ident:"pattern_{}"hellopattern_hello (ident)
fmt:"pattern_{}"hello"pattern_hello" (string)

Custom pipes via #[zyn::pipe]:

#[zyn::pipe]
fn shout(input: String) -> zyn::syn::Ident {
    zyn::syn::Ident::new(&format!("{}_BANG", input.to_uppercase()), zyn::Span::call_site())
}

zyn::zyn!(fn {{ name | shout }}() {})
// name = "hello" → fn HELLO_BANG() {}

Attributes

zyn provides two tools for attribute handling: a derive macro for typed parsing and a proc macro attribute for writing attribute macros.

Typed attribute structs via #[derive(Attribute)]:

#[derive(zyn::Attribute)]
#[zyn("builder")]
struct BuilderConfig {
    #[zyn(default)]
    skip: bool,
    #[zyn(default = "build".to_string())]
    method: String,
}
// users write: #[builder(skip)] or #[builder(method = "create")]

The derive generates from_args, FromArg, and FromInput implementations, as well as a human-readable about() string for error messages.

Auto-suggest

When a user misspells an argument name, zyn automatically suggests the closest known field. No extra setup required:

error: unknown argument `skiip`
  --> src/main.rs:5:12
   |
5  | #[builder(skiip)]
   |           ^^^^^
   |
   = help: did you mean `skip`?

Suggestions are offered when the edit distance is ≤ 3 characters. Distant or completely unknown keys produce only the "unknown argument" error without a suggestion.

Attribute proc macros via #[zyn::attribute]:

#[zyn::attribute]
fn my_attr(#[zyn(input)] item: zyn::syn::ItemFn, args: zyn::Args) -> zyn::TokenStream {
    // args: parsed key=value arguments from the attribute invocation
    zyn::zyn!({ { item } })
}

Testing

Assertions

zyn! returns Output — test both tokens and diagnostics directly:

#[test]
fn generates_function() {
    let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput).unwrap().into();
    let output = zyn::zyn!(fn hello() {});
    let expected = zyn::quote::quote!(fn hello() {});
    zyn::assert_tokens!(output, expected);
}

Diagnostic assertions check error messages from error!, warn!, bail!:

#[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 present, no tokens produced
}

#[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"
}
MacroPurpose
assert_tokens!Compare two token streams
assert_tokens_empty!Assert no tokens produced
assert_tokens_contain!Check for substring in output
assert_diagnostic_error!Assert error diagnostic with message
assert_diagnostic_warning!Assert warning diagnostic
assert_diagnostic_note!Assert note diagnostic
assert_diagnostic_help!Assert help diagnostic
assert_compile_error!Alias for assert_diagnostic_error!

With the pretty feature:

MacroPurpose
assert_tokens_pretty!Compare using prettyplease-formatted output
assert_tokens_contain_pretty!Substring check on pretty-printed output

Debugging

Inspect generated code by adding debug to any zyn attribute macro. Set ZYN_DEBUG to the generated type name (or * for all) to enable output:

#[zyn::element(debug)]
fn greeting(name: zyn::syn::Ident) -> zyn::TokenStream {
    zyn::zyn!(fn {{ name }}() {})
}
ZYN_DEBUG="*" cargo build
note: zyn::element ─── Greeting

      fn {{ name }}() {}
  --> src/lib.rs:1:1

Without injection, props show as {{ name }} placeholders. With the pretty feature, use debug(pretty) for formatted output:

#[zyn::element(debug(pretty))]
fn greeting(name: zyn::syn::Ident) -> zyn::TokenStream {
    zyn::zyn!(fn {{ name }}() {})
}
note: zyn::element ─── Greeting

      fn {{ name }}() {}
  --> src/lib.rs:1:1

Use debug(full) to emit the full generated struct + impl instead of just the body. Combine with pretty: debug(pretty, full).

Supply key = "value" injection pairs to substitute static values for props — useful when the real value isn't known at proc-macro time:

#[zyn::element(debug(name = "Foo", ty = "String"))]
fn setter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
    zyn::zyn!(fn {{ name }}(value: {{ ty }}) -> Self { self })
}

Output: fn Foo(value : String) -> Self { self }. Without injection, props show as {{ name }}, {{ ty }}.

Static injection — inline diagnostic

All macros support debug: #[zyn::element], #[zyn::pipe], #[zyn::derive], #[zyn::attribute].

ZYN_DEBUG accepts comma-separated *-wildcard patterns matched against the generated PascalCase type name:

ZYN_DEBUG="Greeting" cargo build        # exact match
ZYN_DEBUG="Greet*" cargo build          # prefix wildcard
ZYN_DEBUG="*Element" cargo build        # suffix wildcard
ZYN_DEBUG="Greeting,Shout" cargo build  # multiple patterns

Features

FeatureDefaultDescription
derive✓All proc macro attributes: #[element], #[pipe], #[derive], #[attribute], and #[derive(Attribute)]
extExtension traits for common syn AST types (AttrExt, FieldExt, TypeExt, etc.)
prettyPretty-printed token output in debug mode
diagnosticsError accumulation — collect multiple errors before aborting

ext

The ext module adds ergonomic extension traits for navigating syn AST types.

zyn = { features = ["ext"] }
use zyn::ext::{AttrExt, TypeExt};

// check and read attribute arguments
if attr.is("serde") {
    let rename: Option<_> = attr.get("rename"); // → Some(Meta::NameValue)
    let skip: bool = attr.exists("skip");
}

// inspect field types
if field.is_option() {
    let inner = field.inner_type().unwrap();
}

pretty

Pretty-printed debug output and assert_tokens_pretty! / assert_tokens_contain_pretty! assertion macros via prettyplease. See Debugging for usage.

zyn = { features = ["pretty"] }

Pretty debug output

Pretty full output

diagnostics

The diagnostics feature enables error accumulation — collecting multiple compiler errors before aborting, so users see all problems at once instead of one at a time.

zyn = { features = ["diagnostics"] }

Inside any #[zyn::element], #[zyn::derive], or #[zyn::attribute] body, use the built-in diagnostic macros directly — no setup required:

#[zyn::element]
fn my_element(name: zyn::syn::Ident) -> zyn::TokenStream {
    if name == "forbidden" {
        bail!("reserved identifier `{}`", name);
    }

    if name.to_string().starts_with('_') {
        warn!("identifiers starting with `_` are conventionally unused");
    }

    zyn::zyn!(fn {{ name }}() {})
}
MacroLevelBehaviour
error!(msg)erroraccumulates, does not stop execution
warn!(msg)warningaccumulates, does not stop execution
note!(msg)noteaccumulates, does not stop execution
help!(msg)helpaccumulates, does not stop execution
bail!(msg)erroraccumulates and immediately returns

All accumulated diagnostics are emitted together at the end of the element or macro body, so users see every error at once instead of fixing them one by one.

error: reserved identifier `forbidden`
 --> src/main.rs:3:1

error: reserved identifier `forbidden`
 --> src/main.rs:7:1

Performance

Benchmarks confirm the zero-overhead claim: the full pipeline (parse, extract, codegen) matches vanilla syn + quote for both structs and enums. Where zyn replaces external crates, it's faster — case conversion is ~6x faster than heck, and attribute parsing is ~14% faster than darling.

Live benchmark charts on bencher.dev

Discussions

License

MIT

Contributors

aacebo

212 commits

jthillard

3 commits

A4-Tacks

2 commits

chirizxc

1 commits

aacebo/zyn

A procedural macro development framework designed to simplify and add structure to rust macro development

Rust

196

219 commits

updated Sep 18, 2026

See the code

See what people are saying

SourceMessageScoreDate

Moxy - a Rust syntax toolkit designed to bring more cohesion to Rust parsing, tokenization, formatting, diagnostics, and quasi-quoting. (r/rust)

Last year I published [zyn](https://github.com/aacebo/zyn), a template engine for Rust proc macros. I got a lot of great constructive feedback from the community, which inspired me to take the ideas behind it a step further. `moxy` has been written from the ground up with a few goals in mind: # 1.…

34

Sep 26, 2026

README

Getting Started docs.rs crates.io bencher.dev Crates.io Size

A proc macro framework with templates, composable elements, and built-in diagnostics.

[!IMPORTANT] Zyn is being superseded by Moxy. Moxy takes the ideas behind Zyn further by owning the token, AST, parsing, templating, formatting, and diagnostics stack. Some Zyn features are not yet available in Moxy, but are planned to be added soon.

Read Moxy is replacing Zyn for more about the project and the rationale behind the change.

🗺️ Roadmap

cargo add zyn

Table of Contents


Templates

Templates are fully type-checked at compile time — errors appear inline, just like regular Rust code.

Compile-time type safety

The zyn! macro is the core of zyn. Write token output as if it were source code, with {{ }} interpolation and @ control flow directives.

Interpolation — any ToTokens value:

let name = zyn::format_ident!("hello_world");
zyn::zyn!(fn {{ name }}() {})
// → fn hello_world() {}

Pipes — transform values inline:

zyn::zyn!(fn {{ name | pascal }}() {})
// name = "hello_world" → fn HelloWorld() {}

Control flow:

zyn::zyn!(
    @if (is_pub) { pub }
    @for (field in fields.named.iter()) {
        fn {{ field.ident }}(&self) -> &{{ field.ty }} {
            &self.{{ field.ident }}
        }
    }
)

Full template syntax:

SyntaxPurpose
{{ expr }}Interpolate any ToTokens value
{{ expr | pipe }}Transform value through a pipe before inserting
@if (cond) { ... }Conditional token emission
@else { ... }Else branch
@else if (cond) { ... }Else-if branch
@for (x in iter) { ... }Loop over an iterator
@for (N) { ... }Repeat N times
@match (expr) { pat => { ... } }Pattern-based emission
@element_name(prop = val)Invoke a #[element] component

Elements

Elements are reusable template components defined with #[zyn::element]. They encapsulate a fragment of token output and accept typed props.

Define an element:

#[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 it inside any template with @:

zyn::zyn! {
    impl {{ ident }} {
        @for (field in fields.named.iter()) {
            @getter(name = field.ident.clone().unwrap(), ty = field.ty.clone())
        }
    }
}

Elements can also receive extractors — values resolved automatically from proc macro input — by marking a param with #[zyn(input)]:

#[zyn::derive]
fn my_getters(
    #[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
    #[zyn(input)] fields: zyn::Fields<zyn::syn::FieldsNamed>,
) -> zyn::TokenStream {
    zyn::zyn! {
        impl {{ ident }} {
            @for (field in fields.named.iter()) {
                pub fn {{ field.ident | snake | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
                    &self.{{ field.ident }}
                }
            }
        }
    }
}
// Applied to: struct User { first_name: String, age: u32 }
// Generates:
// impl User {
//     pub fn get_first_name(&self) -> &String { &self.first_name }
//     pub fn get_age(&self) -> &u32 { &self.age }
// }

Pipes

Pipes transform interpolated values: {{ expr | pipe }}. They chain left to right:

zyn::zyn!(fn {{ name | snake | ident:"get_{}" }}() {})
// name = "HelloWorld" → fn get_hello_world() {}

Built-in pipes:

PipeInput exampleOutput
snakeHelloWorldhello_world
pascalhello_worldHelloWorld
camelhello_worldhelloWorld
screamingHelloWorldHELLO_WORLD
kebabHelloWorld"hello-world"
upperhelloHELLO
lowerHELLOhello
strhello"hello"
trim__foo__foo
pluraluserusers
singularusersuser
ident:"pattern_{}"hellopattern_hello (ident)
fmt:"pattern_{}"hello"pattern_hello" (string)

Custom pipes via #[zyn::pipe]:

#[zyn::pipe]
fn shout(input: String) -> zyn::syn::Ident {
    zyn::syn::Ident::new(&format!("{}_BANG", input.to_uppercase()), zyn::Span::call_site())
}

zyn::zyn!(fn {{ name | shout }}() {})
// name = "hello" → fn HELLO_BANG() {}

Attributes

zyn provides two tools for attribute handling: a derive macro for typed parsing and a proc macro attribute for writing attribute macros.

Typed attribute structs via #[derive(Attribute)]:

#[derive(zyn::Attribute)]
#[zyn("builder")]
struct BuilderConfig {
    #[zyn(default)]
    skip: bool,
    #[zyn(default = "build".to_string())]
    method: String,
}
// users write: #[builder(skip)] or #[builder(method = "create")]

The derive generates from_args, FromArg, and FromInput implementations, as well as a human-readable about() string for error messages.

Auto-suggest

When a user misspells an argument name, zyn automatically suggests the closest known field. No extra setup required:

error: unknown argument `skiip`
  --> src/main.rs:5:12
   |
5  | #[builder(skiip)]
   |           ^^^^^
   |
   = help: did you mean `skip`?

Suggestions are offered when the edit distance is ≤ 3 characters. Distant or completely unknown keys produce only the "unknown argument" error without a suggestion.

Attribute proc macros via #[zyn::attribute]:

#[zyn::attribute]
fn my_attr(#[zyn(input)] item: zyn::syn::ItemFn, args: zyn::Args) -> zyn::TokenStream {
    // args: parsed key=value arguments from the attribute invocation
    zyn::zyn!({ { item } })
}

Testing

Assertions

zyn! returns Output — test both tokens and diagnostics directly:

#[test]
fn generates_function() {
    let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput).unwrap().into();
    let output = zyn::zyn!(fn hello() {});
    let expected = zyn::quote::quote!(fn hello() {});
    zyn::assert_tokens!(output, expected);
}

Diagnostic assertions check error messages from error!, warn!, bail!:

#[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 present, no tokens produced
}

#[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"
}
MacroPurpose
assert_tokens!Compare two token streams
assert_tokens_empty!Assert no tokens produced
assert_tokens_contain!Check for substring in output
assert_diagnostic_error!Assert error diagnostic with message
assert_diagnostic_warning!Assert warning diagnostic
assert_diagnostic_note!Assert note diagnostic
assert_diagnostic_help!Assert help diagnostic
assert_compile_error!Alias for assert_diagnostic_error!

With the pretty feature:

MacroPurpose
assert_tokens_pretty!Compare using prettyplease-formatted output
assert_tokens_contain_pretty!Substring check on pretty-printed output

Debugging

Inspect generated code by adding debug to any zyn attribute macro. Set ZYN_DEBUG to the generated type name (or * for all) to enable output:

#[zyn::element(debug)]
fn greeting(name: zyn::syn::Ident) -> zyn::TokenStream {
    zyn::zyn!(fn {{ name }}() {})
}
ZYN_DEBUG="*" cargo build
note: zyn::element ─── Greeting

      fn {{ name }}() {}
  --> src/lib.rs:1:1

Without injection, props show as {{ name }} placeholders. With the pretty feature, use debug(pretty) for formatted output:

#[zyn::element(debug(pretty))]
fn greeting(name: zyn::syn::Ident) -> zyn::TokenStream {
    zyn::zyn!(fn {{ name }}() {})
}
note: zyn::element ─── Greeting

      fn {{ name }}() {}
  --> src/lib.rs:1:1

Use debug(full) to emit the full generated struct + impl instead of just the body. Combine with pretty: debug(pretty, full).

Supply key = "value" injection pairs to substitute static values for props — useful when the real value isn't known at proc-macro time:

#[zyn::element(debug(name = "Foo", ty = "String"))]
fn setter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
    zyn::zyn!(fn {{ name }}(value: {{ ty }}) -> Self { self })
}

Output: fn Foo(value : String) -> Self { self }. Without injection, props show as {{ name }}, {{ ty }}.

Static injection — inline diagnostic

All macros support debug: #[zyn::element], #[zyn::pipe], #[zyn::derive], #[zyn::attribute].

ZYN_DEBUG accepts comma-separated *-wildcard patterns matched against the generated PascalCase type name:

ZYN_DEBUG="Greeting" cargo build        # exact match
ZYN_DEBUG="Greet*" cargo build          # prefix wildcard
ZYN_DEBUG="*Element" cargo build        # suffix wildcard
ZYN_DEBUG="Greeting,Shout" cargo build  # multiple patterns

Features

FeatureDefaultDescription
derive✓All proc macro attributes: #[element], #[pipe], #[derive], #[attribute], and #[derive(Attribute)]
extExtension traits for common syn AST types (AttrExt, FieldExt, TypeExt, etc.)
prettyPretty-printed token output in debug mode
diagnosticsError accumulation — collect multiple errors before aborting

ext

The ext module adds ergonomic extension traits for navigating syn AST types.

zyn = { features = ["ext"] }
use zyn::ext::{AttrExt, TypeExt};

// check and read attribute arguments
if attr.is("serde") {
    let rename: Option<_> = attr.get("rename"); // → Some(Meta::NameValue)
    let skip: bool = attr.exists("skip");
}

// inspect field types
if field.is_option() {
    let inner = field.inner_type().unwrap();
}

pretty

Pretty-printed debug output and assert_tokens_pretty! / assert_tokens_contain_pretty! assertion macros via prettyplease. See Debugging for usage.

zyn = { features = ["pretty"] }

Pretty debug output

Pretty full output

diagnostics

The diagnostics feature enables error accumulation — collecting multiple compiler errors before aborting, so users see all problems at once instead of one at a time.

zyn = { features = ["diagnostics"] }

Inside any #[zyn::element], #[zyn::derive], or #[zyn::attribute] body, use the built-in diagnostic macros directly — no setup required:

#[zyn::element]
fn my_element(name: zyn::syn::Ident) -> zyn::TokenStream {
    if name == "forbidden" {
        bail!("reserved identifier `{}`", name);
    }

    if name.to_string().starts_with('_') {
        warn!("identifiers starting with `_` are conventionally unused");
    }

    zyn::zyn!(fn {{ name }}() {})
}
MacroLevelBehaviour
error!(msg)erroraccumulates, does not stop execution
warn!(msg)warningaccumulates, does not stop execution
note!(msg)noteaccumulates, does not stop execution
help!(msg)helpaccumulates, does not stop execution
bail!(msg)erroraccumulates and immediately returns

All accumulated diagnostics are emitted together at the end of the element or macro body, so users see every error at once instead of fixing them one by one.

error: reserved identifier `forbidden`
 --> src/main.rs:3:1

error: reserved identifier `forbidden`
 --> src/main.rs:7:1

Performance

Benchmarks confirm the zero-overhead claim: the full pipeline (parse, extract, codegen) matches vanilla syn + quote for both structs and enums. Where zyn replaces external crates, it's faster — case conversion is ~6x faster than heck, and attribute parsing is ~14% faster than darling.

Live benchmark charts on bencher.dev

Discussions

License

MIT

Contributors

aacebo

212 commits

jthillard

3 commits

A4-Tacks

2 commits

chirizxc

1 commits

Languages

Rust

83.4%

CSS

7.8%

JavaScript

5.8%

Handlebars

3.0%