agentleFS
po4ykapo4yka/rust-skills/skills/rust-macros/SKILL.md

rust-macros

Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub

Use when writing, debugging, or reviewing a Rust macro_rules! declarative macro, a procedural macro (derive, attribute, or function-like) built on syn and quote, or the facade and derive crate pair that ships one. Triggers on "cannot find macro in this scope", "recursion limit reached while expanding", "proc-macro derive panicked", "trailing semicolon in macro used in expression position", "cyclic package dependency", "macro_rules", "write a derive macro", "proc macro", "macro hygiene", "fragment specifier", "syn 3", "cargo expand".

  • Installs packages

5 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use rust-concurrency.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • huialiSkill

    rust-concurrency

    Concurrency and async programming expert. Handles Send, Sync, threads, async/await, tokio, channels, Mutex, RwLock, deadlock prevention, and race condition debugging.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

9 rules. 0 are common, 5 are distinctive, and 4 are very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 5

    Distinctive

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Every quoted message and number below comes from rustc, cargo, and Clippy 1.98.1, edition 2024.

    line 9
  2. Very specific:

    Do not copy nightly macro syntax.

    line 28
  3. Distinctive:

    Read the expansion before you reason about why a macro fails.

    line 55
  4. Very specific:

    Every metavariable in a matcher needs a fragment specifier.

    line 207
  5. Distinctive:

    Prefer a shape that halves the input per step, because expansion time grows with the count.

    line 236
  6. Very specific:

    Remove the ; after the last expression.

    line 249
  7. Very specific:

    Keep the #[proc_macro_derive] function thin.

    line 285
  8. Distinctive:

    Read references/derive-macro-crates.md when you write a derive or set up the facade and derive pair: skeleton, dependency direction, …

    line 290
  9. Distinctive:

    Use these skills by name when they are installed.

    line 313

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust macros

Every quoted message and number below comes from rustc, cargo, and Clippy 1.98.1, edition 2024.

Decide whether you need a macro

You wantReach forWhy not a macro
One body, many typesA generic function, or a blanket implMonomorphization already does this
A constant computed at build timeconst fn, or a const { } blockYou add no syntax
Code generated from a schema or a data filebuild.rs plus include!The input is not Rust tokens
Items or an expression chosen per target or featurecfg_select! (std, Rust 1.95+)No hand-written macro and no cfg-if dependency
Repeated syntax inside one cratemacro_rules!—
The same trait impl per type, derived from the type's shapeA derive macro—
An item rewritten or wrappedAn attribute macro (procedural)—

A macro costs compile time, a second crate in the procedural case, diagnostics that point at the invocation instead of the defect, and an IDE that cannot look inside. Take a non-macro row when one fits.

Do not copy nightly macro syntax. As of Rust 1.98.1, a macro_rules! arm written attr() (...) => ... fails with error[E0658]: `macro_rules!` attributes are unstable, an arm written derive() (...) => ... fails with error[E0658]: `macro_rules!` derives are unstable, and ${count($x)} fails with error[E0658]: meta-variable expressions are unstable.

Route the symptom

Symptom or taskSection
error: cannot find macro ... in this scopeScope is textual
error[E0425]: cannot find function ... or error[E0433]: cannot find ... in the crate root in another crate, from an exported macroEmit absolute paths
An error on a caller's type or alias that says this error originates in the macroEmit absolute paths
error[E0425]: cannot find value ..., with a macro-hygiene noteHygiene stops at bindings
error[E0428]: the name ... is defined multiple times from two invocationsHygiene stops at bindings
error: `$x:frag` is followed by ..., which is not allowed, or missing fragment specifierFragment specifiers
error: no rules expected ... when one macro forwards to anotherFragment specifiers
error: recursion limit reached while expandingFragment specifiers
error: trailing semicolon in macro used in expression positionTrailing semicolons
format argument must be a string literal, or there is no argument named ...Build a format string
A proc-macro crate error, proc-macro derive panicked, or an undeclared helper attributeProcedural macro crates
Choosing a syn version, or E0599/E0609 on BareFn, unsafety, or guardPick the syn major
error: cyclic package dependency from a derive-crate back edgereferences/derive-macro-crates.md
error[E0405]: cannot find trait ... in module ..., from a derive macroreferences/derive-macro-crates.md
You must see what the macro produced, or prove it worksRead the expansion and verify

Read the expansion and verify

Read the expansion before you reason about why a macro fails.

# Nightly toolchain. Prints the whole crate after expansion.
cargo +nightly rustc --locked -p <crate> --profile=check -- -Zunpretty=expanded

# One file, no cargo.
rustc +nightly --edition 2024 -Zunpretty=expanded src/main.rs

# Works on the stable toolchain: it sets RUSTC_BOOTSTRAP=1 for the -Zunpretty call.
cargo install --locked cargo-expand
cargo expand --locked --package <crate> <path::to::item>

Expansion to text is lossy: it prints hygienic identifiers as plain names. Do not paste the output back as source. It can fail to compile, or compile and behave differently.

ClaimCheck
The expansion compiles and behaves in another crateAn invocation in tests/*.rs of the macro crate: cargo test --locked -p <macro-crate>
Emitted paths survive the caller's namesA test module that defines mod facade {} and type Result<T> = ...;, then invokes the macro
A rejected input produces the intended messagetrybuild compile-fail cases, same command
No crate:: path and no metavariable inside unsafecargo clippy --locked --all-targets -- -D warnings reports no crate_in_macro_def or macro_metavars_in_unsafe. The second lint has preconditions
No trailing ; in an expression-position armOne invocation in expression position inside the defining crate, for example in a #[cfg(test)] mod tests
The macro is usable through the facadeA test in the facade crate: cargo test --locked -p <facade-crate>

rustc does not report most lints, for example unused_variables and unsafe_code, in the expansion of another crate's macro. Lint the expansion through an invocation inside the defining crate.

Set up trybuild with trybuild = "1" under [dev-dependencies] and a #[test] that calls trybuild::TestCases::new().compile_fail("tests/ui/*.rs"). Run TRYBUILD=overwrite cargo test --locked to write the .stderr files, and review each one before you commit it.

A green run proves only the input shapes you tested. Add one case per supported shape: a generic type, a PhantomData<T> field, a unit struct, and each rejected shape. Read references/review-checklist.md when you review a finished macro change.

Emit absolute paths

A macro expands in the caller's module. Every path in its output resolves there, against the caller's imports and local items.

The path namesEmitBecause
An item of the macro's own crate$crate::render(...)A bare render fails in every other crate with E0425. crate::render names the caller's crate, and clippy::crate_in_macro_def flags it
A standard-library item::core::result::Result. For an alloc item, put pub use alloc::vec::Vec; in $crate::__private and emit $crate::__private::Vec. Emit ::std:: only for an item that neither core nor alloc has, and only when no #![no_std] caller is supported. Do not emit ::alloc::A bare Result resolves to the caller's type Result<T> = ... alias. ::std:: fails with E0433 in a #![no_std] caller. ::alloc:: fails with E0433 in every caller that does not write extern crate alloc;, ordinary std crates included, because alloc is not in their extern prelude (verified on Rust 1.98.1)
An item of a dependency of the macro crateRe-export it (#[doc(hidden)] pub mod __private { pub use dep_name; }) and emit $crate::__private::dep_name::Item. From a derive, re-export it from the facade and emit ::facade::__private::dep_name::Item::dep_name resolves in the caller's crate, and the caller may not depend on dep_name
The derive attribute#[::core::derive(...)] when the macro crate's rust-version is 1.96 or later, else #[::core::prelude::v1::derive(...)]derive also resolves at the call site. Rust 1.95 rejects ::core::derive with E0433

Emit ::dep_name:: only for a crate that the caller must depend on directly, such as the facade. A tests/*.rs invocation does not catch a ::dep_name:: path, because an integration test sees every dependency of its package. Do not put $crate:: in front of a path the caller passed in as a metavariable. That path names an item in the caller's crate.

A missing $crate passes in one place only: a module of the defining crate that already has the item in scope. It fails in every downstream crate, in a #[cfg(test)] mod tests that does not use super::*, and in your own tests/*.rs. A bare standard-library name fails only in a caller that shadows it, for example E0107 on a bare Result<T, E> under a caller's type Result<T> alias. Read references/declarative-macros.md when you need that reproduction as a test. A derive macro follows the same rule with ::facade::Trait paths; see references/derive-macro-crates.md.

Keep caller expressions out of unsafe blocks

unsafe { f($e) } evaluates the caller's expression $e inside your unsafe block, so the caller can write unsafe operations with no unsafe keyword at the call site. Evaluate each metavariable into a local before the block: let v = $e;, then unsafe { f(v) }. clippy::macro_metavars_in_unsafe flags the unbound form. It fires only on an invocation in the defining crate, such as a #[cfg(test)] mod tests; an invocation in tests/*.rs does not trigger it. By default the lint checks only #[macro_export] macros. For a private macro, set warn-unsafe-macro-metavars-in-private-macros = true in clippy.toml.

A local does not help when the block's soundness depends on the value the caller passes. Then emit a call to an unsafe fn outside any unsafe block, so the caller must write unsafe.

A downstream #![forbid(unsafe_code)] does not reject an unsafe block that another crate's macro emits. The macro crate owns the soundness of every unsafe block in its expansion.

Scope is textual

A macro_rules! macro is the one item in Rust whose definition must appear before its use in source order. A refactor that moves the definition down the file breaks the build, and the message names no missing import.

error: cannot find macro `hello` in this scope
  |             ^^^^^ consider moving the definition of `hello` before this call
note: a macro with the same name exists, but it appears later

The same rule applies across modules. A macro defined in mod a is invisible in mod b, even when b comes later:

error: cannot find macro `only_here` in this scope
  = help: have you added the `#[macro_use]` on the module/import?

Four ways to widen the scope. Prefer the first: it keeps the macro private and needs no crate-root placement.

GoalWriteEffect
Use it in another module, crate-privatepub(crate) use name; after the definitionThe macro becomes an ordinary item of that module, reachable by path
Use it in later modules of the same crate#[macro_use] mod a;The macro joins the crate-root macro scope from that point on
Import it by path anywhere in the crate#[macro_export] on the definition, then use crate::name;The macro lands at the crate root, whatever module defines it
Export it to other crates#[macro_export], then use thatcrate::name; in the callerSame crate-root placement, now public

#[macro_export] puts the macro in the public API of the crate and at its root. Do not reach for it to cross a module boundary inside one crate.

Hygiene stops at bindings

Hygiene is not a general namespace shield. It covers exactly the identifiers rustc can rename.

Identifier a macro createsHygienicConsequence
let binding, function parameterYesThe caller cannot name it, and you cannot hand a name back
Loop label, block labelYesSame as a let binding
LifetimeNoA generated <'a> and a caller's 'a are the same name. Take the lifetime name from a metavariable, or pick one no caller uses
struct, enum, fn, const, mod, staticNoThe caller sees it, and two invocations collide
Type name, field name, method nameNoSame
Macro nameNoSame

A generated local is invisible to the caller: E0425 with help: an identifier with the same name is defined here, but is not accessible due to macro hygiene. A fixed item name collides on the second invocation with error[E0428]: the name `Inner` is defined multiple times.

Two fixes. Derive every generated item name from a metavariable, or wrap the private helpers in an anonymous const _: () = { ... }; block. Each expansion of that block gets its own item namespace, and an impl inside it still applies to the outer type. Read references/declarative-macros.md when you need the reproductions or a working const _ example.

Fragment specifiers

Follow sets are checked at definition time

rustc rejects an illegal token sequence when it reads the macro_rules! item, before anyone invokes the macro. Design the separators before you write the arms. Only =>, ,, or ; may follow an expr or stmt fragment, and | cannot follow pat; use pat_param when | must be your separator. Read references/declarative-macros.md when you pick a separator after pat, ty, path, or vis; it has the full follow-set table.

error: `$e:expr` is followed by `$s:stmt`, which is not allowed for `expr` fragments
  = note: allowed there are: `=>`, `,` or `;`

Every metavariable in a matcher needs a fragment specifier. ($x) => fails with error: missing fragment specifier in every edition since Rust 1.89.

A parsed fragment is opaque to the next matcher

Once a matcher captures $e:expr, the capture is a single AST node, not tokens. Forwarding it to a macro that expects $i:ident fails even when the caller wrote a bare identifier.

error: no rules expected `expr` metavariable
note: while trying to match meta-variable `$i:ident`

Capture as tt in the outer macro when you must forward to an inner matcher, and parse only at the last step that needs the fragment.

Edition 2024 widened expr

In edition 2024 $e:expr also matches const { 1 + 1 } and the underscore expression _. Edition 2021 rejects the const block with error: no rules expected keyword `const` and the underscore with error: no rules expected reserved identifier `_` . An arm that assumes the capture is a value expression can now receive _, which is not one. Use expr_2021 to keep the edition 2021 match set.

The recursion limit is 128 expansions

A tt-muncher costs one expansion per token. Measured with a counting muncher: 127 input tokens compile, 128 fail with error: recursion limit reached while expanding `count!` . A crate-root #![recursion_limit = "256"] is legitimate for a deliberately recursive macro. Prefer a shape that halves the input per step, because expansion time grows with the count.

A trailing semicolon breaks expression position

The braces around an arm body are delimiters, not a block. () => { 42; } expands to the statement 42;, and let x = val!(); needs an expression. The semicolon_in_expressions_from_macros lint rejects it, deny-by-default since Rust 1.91. That lint fires only when the macro is defined in the crate that invokes it. Up to Rust 1.98.1, an invocation from tests/*.rs or a downstream crate compiles without a warning. Rust 1.99 adds semicolon_in_expressions_from_non_local_macros, warn-by-default and part of future_incompatible, for a macro from another crate. On 1.99.1 a downstream build under -D warnings fails on the defect.

Remove the ; after the last expression. When the body needs statements, add an inner block with double braces: () => {{ let base = 40; base + 2 }}.

Build a format string

A format string must be a literal token: forward the caller's literal with $fmt:literal, or build one with concat!. A format string from concat! cannot capture variables inline. The $(,)? tail accepts an optional trailing comma; add it to every repetition that a caller writes across several lines. Read references/declarative-macros.md when a macro builds or forwards a format string.

Procedural macro crates

A procedural macro lives in its own crate:

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1"
quote = "1"
syn = "3"

The crate exports macro functions and nothing else, and it cannot invoke its own macros. Neither error carries an error[Ennnn] code or names the proc-macro = true key.

SymptomCauseFix
error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, ...A pub struct, pub fn, or pub use in the macro crateKeep helpers private. Move a shared type to a plain support crate that the macro crate depends on, never to the facade
error: can't use a procedural macro from the same crate that defines it; a test build adds help: you can define integration tests in a directory named `tests` A #[cfg(test)] mod tests in src/lib.rs invokes the macroPut macro tests in tests/, which compiles as a separate crate
error: proc-macro derive panicked with help: message: ... and a span on the whole deriveA panic!, unwrap, or expect in the macroReturn a syn::Error at the offending span; see below
error: cannot find attribute ... in this scope on a fieldThe derive reads a helper attribute it did not declareList it: #[proc_macro_derive(Checked, attributes(checked))]
error[E0659]: `inline` is ambiguous on a field; ambiguous_derive_helpers warning in the macro crateA helper attribute shares a built-in attribute's nameName the helper after the derive, for example checked

Keep the #[proc_macro_derive] function thin. Parse with syn::parse_macro_input!, put the logic in fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>, and map an error with .unwrap_or_else(syn::Error::into_compile_error). The user then sees a normal diagnostic on the span you chose.

Read references/derive-macro-crates.md when you write a derive or set up the facade and derive pair: skeleton, dependency direction, generic bounds, and the missing-derive error.

Pick the syn major

syn 3 is current (3.0.0 on 2026-07-18). As of 2026-09, serde_derive, thiserror-impl, tokio-macros, and clap_derive require syn ^3, while other popular macro crates still use syn 2. Each major in the tree is a separate build of a large crate.

cargo tree --locked -e normal,build -i syn

One version prints its dependents. Two majors print error: specification `syn` is ambiguous and list both. Run cargo tree --locked -e normal,build -i syn@2 to list the dependencies that still build syn 2. Use the major the tree already builds. When the tree has both or none, use syn = "3". syn-2 code fails on syn 3 with E0599, E0609, or E0432 on renamed items such as Type::BareFn, Signature::unsafety, and Arm::guard. Read references/syn-3.md when you port a macro crate or fix those errors.

Related skills

Use these skills by name when they are installed.

SkillBoundary
rust-crate-architectureWhich crates exist and which way normal dependencies point, apart from the macro crate pair
rust-lintsLint policy for the code you write
rust-compiler-errorsDiagnostics that do not come from a macro
rust-unsafeSoundness of the unsafe code a macro emits
rust-disciplineAPI design of the trait a derive implements, including the delegation macro pattern
rust-serde#[derive(Serialize, Deserialize)] and its container and field attributes as a contract
rust-test-toolsGolden tests and CI wiring. trybuild guidance stays in this skill

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.