Expand description
§batch-impl
v0.6.3 (2026-08-05) — 0.6.2: receiver-kind filters (@all_ref_methods etc.) + #blanket static-method delegation + span-based diagnostics; error messages fully in English. 0.6.3: doc fix.
A procedural macro crate that batch-generates impl blocks for Rust traits — one line of DSL, expanded into N impls.
use batch_impl::batch_impl;
// One body, one impl for each of the 4 types
#[batch_impl(<T> Sortable<T> [Box, Rc]^Vec<T> where T: Ord {
fn is_sorted(&self) -> bool { self.windows(2).all(|w| w[0] <= w[1]) }
})]
trait Sortable<T> { fn is_sorted(&self) -> bool; }
// → impl<T> Sortable<T> for Box<Vec<T>> where T: Ord { ... }
// → impl<T> Sortable<T> for Rc<Vec<T>> where T: Ord { ... }
// One line generates a single 4-generic tuple impl (length ranges use `()^1..=4`)
#[batch_impl(()^4)]
trait TupleTrait {}
// → impl<A, B, C, D> TupleTrait for (A, B, C, D) {}§Why use it
Hand-writing the same trait implementation for multiple types means repetition: the signature is copied N times, the body is copied N times, generic parameters and associated types are each written separately, and changing one place misses three. batch-impl puts the quantity of impls into a description outside the human brain:
- One source of truth: the trait definition is written only once (signature/generics/bound/where constraints), the DSL only writes “which types × what implementation”, and the macro fills in the rest — signatures, generic bounds, associated type bindings, and even trait-level where constraints are automatically inherited from the trait definition, fully equivalent to hand-written code.
- One-line matrix:
[...]lists,^/-application,()^Ntuple generation — one DSL line describes a “type matrix”, and the macro generates one impl per cell. - Batch, but hand-written in feel:
{ body }is ordinary Rust code,#directives automatically copy signatures, and the generated impl is token-for-token equivalent to hand-written code — whatever rustc can verify, it can verify.
A real scenario (see examples/simplify.rs): 12 numeric types + 4 wrapper types + 4 tuples + some miscellaneous = 29 impls from about 15 lines of DSL, versus about 80 lines by hand.
§Mental model
What you write is a description of a “type matrix”, and batch-impl generates an impl for every cell of the matrix:
#[batch_impl( <impl-generics> TraitName<trait-generics> target-type matrix { body }? )]| Symbol | Meaning | Intuition |
|---|---|---|
^ / - | apply: apply the left container/modifier to the right type | the same operation, only associativity differs |
[A, B] | list | horizontal expansion (Cartesian product) |
(A, B) | tuple | permutations (ordered pairs) |
#name | directive: auto-copy the item signature from the trait definition | the body doesn’t hand-write signatures |
^ and - are the same operation (the left side is a modifier/container, the right side is the target type), differing only in associativity:
^is right-associative, chaining produces nesting:Box^Box^T=Box<Box<T>>,HashMap^K^V=HashMap<K<V>>-is left-associative, chaining accumulates arguments:HashMap-K-V=HashMap<K, V>,fn(A, B)-C=fn(A, B) -> C
So which one to pick depends only on the grouping shape you want: use ^ to nest, use - to list arguments side by side.
[A, B]^[X, Y] = a 2×2 matrix (4 impls); (T1, T2)^2 = permutations (4 ordered pairs).
§Quick start
[dependencies]
batch-impl = "0.6.3"Requires Rust 2024 edition or newer.
use batch_impl::batch_impl;
// 1. Define the trait; the method signature is written only once
trait Describe { fn describe(&self) -> String; }
// 2. Write one DSL line: target type + body (the signature is auto-copied from the trait via #name)
#[batch_impl(
[usize, isize] #name{"number"},
String #name{"string"}
)]
trait Tagged { fn name(&self) -> &str; }
// → impl Tagged for usize { fn name(&self) -> &str { "number" } }
// → impl Tagged for isize { fn name(&self) -> &str { "number" } }
// → impl Tagged for String { fn name(&self) -> &str { "string" } }
// 3. 0.6.2: one-line blanket — delegation impls for every wrapper type
// (instance methods forward via deref; @all_ref_methods selects only
// reference-receiver methods, by-value ones keep the trait default)
#[batch_impl(#blanket(@all_ref_methods){&, Box, Rc})]
trait Describe2 { fn describe(&self) -> String; }
// → impl<T> Describe2 for &T where T: Describe2 { fn describe(&self) -> String { (**self).describe() } }
// → impl<T> Describe2 for Box<T> where T: Describe2 { ... }
// → impl<T> Describe2 for Rc<T> where T: Describe2 { ... }§Feature overview
| Feature | In one sentence | Tutorial chapter |
|---|---|---|
Side-by-side lists [A, B] | Implement for multiple types at once, body reused | Lists and body |
^ / - operators | Right/left associativity of the same operation: nesting vs. accumulation | Operators |
| Generic automation | A<> copied as-is, same-name inheritance, trait where-clause inheritance | Generic automation |
| Associated type bindings | Iter<Item=T> → type Item = T; | Associated types |
Directive system #name/#fill/#delegate | Auto-copy signatures, batch-fill bodies, delegate calls | Directive system |
Blanket delegation #blanket | Generate delegated impls from a wrapper matrix in one line (any wrapper + :N, generic traits, assoc projections, wrapper where predicates, static methods forwarded via t) | Directive system |
| Open extension | Unknown #name(args){body} is handed to your macro with the same name | Directive system |
@ constants | Built-in families @uint/@scalar/@u8..u128 + @trait/@all family/@Cow + batch_trait! customization (lazy expansion, chained references) | Constant system |
Unified macro-meta layer @ | # keeps only directive names; scope selection (@all family, incl. required/default and receiver filters) and positional references (@N) belong to the macro-meta layer | Constant system |
where{...} | Unified constraint container (<> keeps only names), blanket constraints merged side by side | where clauses |
| Tuple generation | ()^3, (T,)^N, Cartesian product, ranges | Tuple generation |
| fn types / unsafe / pointers / attributes | Full support for type-level modifiers | Modifiers |
§Next steps
- Full tutorial:
docs/tutorial.md(progressive, from a one-line impl to advanced matrix combinations) - Three entry points:
#[batch_impl](includes the trait) /#[batch_impl_only](impls only) /batch_trait!(batch-generate for an already declared trait, multi-section support) - Examples:
examples/quickstart.rs(feature demo),examples/simplify.rs(a real scenario with 29 impls ≈ 15 lines of DSL) - Developers: internal architecture in
docs/architecture.md, development changelog indocs/dev-changelog.md
§License
MIT OR Apache-2.0
§batch-impl Tutorial
v0.6.3 — on top of 0.6.1, 0.6.2 adds: filtering by receiver kind (@all_ref_methods / @all_value_methods / @all_static_methods), #blanket static-method delegation, and span diagnostics; error messages are fully in English. 0.6.3 is a doc fix.
A progressively-learned DSL: start from a single impl line and work up to advanced matrix composition. All examples are compilable code; the product of every step is ordinary Rust — the impls the macro generates are token-for-token equivalent to handwritten ones.
§1. Starting from a Single impl
#[batch_impl(...)] is annotated on a trait definition; every spec in its arguments generates one impl:
#[batch_impl(usize, isize, f32, f64)]
trait Numeric {}
// → impl Numeric for usize {}
// → impl Numeric for isize {}
// → impl Numeric for f32 {}
// → impl Numeric for f64 {}The skeleton of a spec:
<impl-generics> TraitName<trait-generics> target type { body }?| Part | Example | When needed |
|---|---|---|
<impl generics> | <T>, <T: Clone>, <const N: usize> | when the impl block needs generic parameters |
TraitName<trait generics> | MyTrait<T>, MyTrait<Vec<T>> | when the trait definition has generic parameters |
| target type | usize, Vec<T>, &str | required |
{ body } | { fn m(&self) -> usize { 0 } } | when you need a custom body |
Separate multiple specs with ,: #[batch_impl(usize, isize)].
§2. Lists and body
§Side-by-side lists [A, B]
One body is reused for all target types:
#[batch_impl([usize, isize, f32] {
fn tag(&self) -> &'static str { "number" }
})]
trait Tagged { fn tag(&self) -> &'static str; }
// → impl Tagged for usize { fn tag(&self) -> &'static str { "number" } }
// → impl Tagged for isize { ... }
// → impl Tagged for f32 { ... }§Merging per-item and shared bodies
List items can have their own bodies, which merge with a shared body:
#[batch_impl(
[usize { fn name() -> &'static str { "usize" } },
isize { fn name() -> &'static str { "isize" } }]
{ fn zero() -> Self { 0 } }
)]
trait Zero {
fn zero() -> Self;
fn name() -> &'static str;
}
// → impl Zero for usize { fn zero() -> Self { 0 } fn name() -> &'static str { "usize" } }
// → impl Zero for isize { fn zero() -> Self { 0 } fn name() -> &'static str { "isize" } }§3. The ^ and - Operators
^ and - are the same operation: the left side is a modifier/container, the right side is the target type. They differ only in associativity:
^ is right-associative (nesting), - is left-associative (accumulating arguments).
Precedence, low to high: ; < , < - < ^; () grouping sits above all operators.
| Syntax | Expands to |
|---|---|
Box^T | Box<T> |
Box^<X,Y> | Box<X, Y> (multi-parameter container) |
Box^Box^T | Box<Box<T>> (right-associative nesting) |
HashMap<K>^V | HashMap<K, V> (prefilled generics appended) |
&^Box^T | &Box<T> (modifiers chained) |
Vec-u32 | Vec<u32> |
HashMap-u32-String | HashMap<u32, String> (left-associative accumulation) |
fn^(A,B)-C | fn(A,B)->C |
[Box, Vec]^T | Box<T>, Vec<T> |
Box^[T1, T2] | Box<T1>, Box<T2> |
[Box, Vec]^[T1, T2] | Cartesian product, 4 entries total |
[HashMap<K>, Vec<K>]^V | HashMap<K, V>, Vec<K, V> |
Note:
Box^Vec-u32is wrong (it would be read asBox<Vec, u32>); writeBox^Vec^u32instead.
Operand strictness:
^/-/,require operands on both sides —A^,^A,-A,,A,A,,Ball raisecompile_error!; only a trailing comma (A,/[A, B,]) is allowed. Brackets such as();/[]are real tokens, not empty operands.;stays lenient as thebatch_trait!section boundary.
§4. Generic Declarations
#[batch_impl(<T> Vec<T>)]
trait Collection {}
// → impl<T> Collection for Vec<T> {}Bound syntax convention (since 0.6.1): <> holds only names; bounds all go into where{...} —
#[batch_impl(<T> Named<T> Vec<T> where{T: Clone} { fn n(&self) -> usize { self.len() } })]
trait Named<T: Clone> { fn n(&self) -> usize; }<T: Clone> (inline bound) is still supported (bounds from the trait definition are inherited automatically when none are written), but once the bound container is uniformly where, merging multiple bounds is just “juxtaposing predicates” (the macro only concatenates tokens, zero analysis) — that is why a blanket’s T: Trait and the wrapper predicate merge naturally.
§Nested generic merging
Each list item declares its own impl generics, automatically merged into the impl block:
#[batch_impl(<T> Describe<T> [Vec<T>, <U> HashMap<T, U>] {
fn describe(&self) -> String { format!("len={}", self.len()) }
})]
trait Describe<T> { fn describe(&self) -> String; }
// → impl<T> Describe<T> for Vec<T>
// → impl<T, U> Describe<T> for HashMap<T, U>§const generics
#[batch_impl(<const N: usize> ConstGeneric<N> [i32; N] {
fn len_const(&self) -> usize { N }
})]
trait ConstGeneric<const N: usize> { fn len_const(&self) -> usize; }
// → impl<const N: usize> ConstGeneric<N> for [i32; N] { ... }§5. Generic Automation (the trait definition is the single source of truth)
§A<> — copy the trait generics verbatim
An empty argument list means “arguments and bounds all come from the trait definition”:
#[batch_impl(Foo<> ())]
trait Foo<T: Clone> {}
// → impl<T: Clone> Foo<T> for ()Available only in #[batch_impl] / #[batch_impl_only] (both need the trait definition); batch_trait! has no trait definition, so A<> passes through verbatim.
§A<bounds> — the same verbatim copying
Pure associated-type bindings (A<Item=T>, no positional arguments) likewise copy the positional arguments and keep bindings verbatim:
#[batch_impl(Foo<Item=T> ())]
trait Foo<T: Clone> { type Item; }
// → impl<T: Clone> Foo<T> for () { type Item = T; }A<T, Item=U> with positional parameters is ordinary DSL syntax (not expanded).
§Same-named inheritance for unwritten bounds
impl parameters correspond to trait parameters “by position in the trait arguments”; a parameter with the same name and no written bound inherits:
#[batch_impl(<T> Foo<T> Vec<T> { fn get(&self) -> T { self[0].clone() } })]
trait Foo<T: Clone> { fn get(&self) -> T; }
// → impl<T: Clone> Foo<T> for Vec<T> { ... }Lifetime bounds (<'a, T> + trait Foo<'a, T: 'a> → impl<'a, T: 'a>), 'static, and mixed bounds (Clone + 'a) are all inherited.
§Inheriting trait-level where clauses
The predicates of trait Foo<T> where T: Clone are inherited in all forms:
#[batch_impl(<T> Foo<T> ())]
trait Foo<T: Clone>
where
T: Ord,
{
}
// → impl<T: Clone + Ord> Foo<T> for ()- Single-parameter predicates (
T: Clone) merge into the bound (inline + where concatenation); the<T>andA<>forms are equivalent; - All other predicates pass through verbatim into the impl’s where clause:
T::Item: Clone,Vec<T>: ..., lifetime predicates ('a: 'b), and so on are all covered.
#[batch_impl(<T> Foo<T> ())]
trait Foo<T>
where
T: IntoIterator,
T::Item: Clone,
{
}
// → impl<T: IntoIterator> Foo<T> for () where T::Item: Clone§Renaming = an explicit error, never silent
An argument X that maps to a parameter T (with a bound) under a different name, or an inherited bound/predicate that refers to a parameter name such as 'a/U while the impl does not declare the same name — all raise compile_error! with guidance (rename, or write the bound by hand). To use a different name, write <X: ...> yourself.
The macro does not interfere with parameters that already have written bounds (whether T: B implies T: Clone is verified by rustc, e.g. the supertrait relationship trait B: A).
§6. Concise Associated Types
The Name=value syntax binds an associated type inside the trait’s generic arguments:
#[batch_impl(<T> Iter<Item=T> Vec<T> {
fn count(&self) -> usize { self.len() }
})]
trait Iter {
type Item;
fn count(&self) -> usize;
}
// → impl<T> Iter for Vec<T> { type Item = T; fn count(&self) -> usize { self.len() } }Multiple associated types and generic constraints are supported:
#[batch_impl(<T, U> Pair<First=T, Second=U> (T, U))]
trait Pair {
type First;
type Second;
}
#[batch_impl(<T: Clone> CloneIter<Item=T> Vec<T> {
fn first(&self) -> T { self[0].clone() }
})]
trait CloneIter {
type Item;
fn first(&self) -> Self::Item;
}§7. The Directive System
The # directives expand during preprocessing, reading item signatures/types automatically from the trait definition — no need to hand-write signatures in the body.
§#name{body} — assigning a single item (fn / const / type automatically pick the output format)
#[batch_impl(usize #to_str{"usize"})]
trait ToString { fn to_str(&self) -> &str; }
// → impl ToString for usize { fn to_str(&self) -> &str { "usize" } }
#[batch_impl(usize #MAX_SIZE{1024})]
trait HasConst { const MAX_SIZE: usize; }
// → impl HasConst for usize { const MAX_SIZE: usize = 1024; }
#[batch_impl(usize #Item{u32})]
trait HasType { type Item; }
// → impl HasType for usize { type Item = u32; }§#fill(methods){body} — one body for many methods
#[batch_impl(usize #fill(name, kind){"usize"})]
trait Describable { fn name(&self) -> &str; fn kind(&self) -> &str; }
// → generates a { "usize" } body for each of name and kindSpecial markers: @all (all items), @all_methods (fn only), @all_constants (const only), @all_types (type only).
Filtering by default-implementation state (new in 0.6.1): trait items are split into “has a default implementation” (fn with a default body / const with a default value / type with a default type) and “no default implementation” (required — the impl must provide it). @all_required* / @all_default* select each side:
| Marker | Selected scope |
|---|---|
@all_required_methods | only methods without a default implementation (the impl must provide them) |
@all_default_methods | only methods with a default implementation (the impl may omit them) |
@all_required / @all_default | all items in the respective state (fn + const + type) |
@all_required_constants / @all_default_constants | consts in the respective state |
@all_required_types / @all_default_types | types in the respective state (note: a default associated type type T = u8; is a nightly feature (associated_type_defaults, E0658 on stable) — @all_default_types is only usable on nightly; the type T; declaration for @all_required_types works on stable) |
Using @all_required_methods alone means “implement only the required ones; default methods keep the trait’s default implementation” (more precise than excluding one by one with @all + -name); @all_default_methods must be combined with the required side or handwritten items (filling only default methods leaves the required ones missing → E0046). required ∪ default = all.
The three directives (#fill/#delegate/#blanket) and - exclusion (-@all_default_methods) all work with these.
// required ones get 1; default methods are overridden with 2
#[batch_impl(usize #fill(@all_required_methods){1} #fill(@all_default_methods){2})]
trait MixDefault {
fn required(&self) -> u32;
fn optional(&self) -> u32 { 100 } // default implementation, overridden by @all_default_methods
}// implement only the required ones; default methods keep the trait defaults
#[batch_impl(u64 #fill(@all_required_methods){3})]
trait KeepDefault {
fn required(&self) -> u32;
fn optional(&self) -> u32 { 7 }
}Filtering by receiver kind (new in 0.6.2): trait methods are split into three kinds by receiver shape —
&self / &mut self (references), self (by value, including typed receivers such as self: Box<Self>),
and no receiver (associated functions / static methods):
| Marker | Selected scope |
|---|---|
@all_ref_methods | &self / &mut self methods |
@all_value_methods | self (incl. typed receivers) methods |
@all_static_methods | associated functions (no receiver) |
A typical use case is blanket: by-value delegation semantics depend on the wrapper’s Deref/move capability, which
cannot be told apart at expansion time — use @all_ref_methods to delegate only reference methods, and by-value
methods keep the trait’s default implementation:
#[batch_impl(u8 { fn by_ref(&self) -> u8 { *self } })]
#[batch_impl(#blanket(@all_ref_methods){Box})]
trait RecvB {
fn by_ref(&self) -> u8;
fn by_val(self) -> u8 where Self: Sized { 0 }
}
// → impl<T> RecvB for Box<T> where T: RecvB {
// fn by_ref(&self) -> u8 { (**self).by_ref() } // delegated
// // by_val is not generated → Box<T> uses the trait's default impl
// }
// note: the `self` receiver in a default impl requires `where Self: Sized`The three markers work in #fill / #delegate / #blanket and - exclusions alike
(e.g. #fill(@all_methods, -@all_value_methods) = only reference + static methods).
§List subtraction -name
In the arguments, a - prefix marks an exclusion (the keep-list minus the exclude-list; exclusions win).
Used for “implement everything except one item”:
#[batch_impl(usize #fill(@all,-skip_me){0})]
trait HasDefault {
fn keep_me(&self) -> u32;
fn skip_me(&self) -> u32 { 999 } // default implementation, kept when excluded
const VALUE: u32;
}
// → impl HasDefault for usize {
// fn keep_me(&self) -> u32 { 0 }
// const VALUE: u32 = 0;
// // skip_me is not generated; the trait's default impl is used
// }- may be followed by an identifier (-foo) or an @all family marker (-@all_methods = exclude all methods):
#fill(@all,-@all_methods) = only const + type items. It also applies to #delegate
(#delegate(@all,-foo){target}). An empty result after exclusion, or a missing target after -, raises compile_error!.
- only takes effect in directive argument domains and does not interfere with the type DSL’s - concatenation operator.
§#delegate(methods){target} — delegation
Delegates methods to same-named calls on the target expression:
// Vec<u32> gets its body via #name; Box<Vec<u32>> delegates to it
#[batch_impl(
Vec<u32> #d_len{self.len()},
Box^Vec^u32 #delegate(d_len){**self}
)]
trait MyLen { fn d_len(&self) -> usize; }
// → impl MyLen for Box<Vec<u32>> { fn d_len(&self) -> usize { (**self).d_len() } }
// blanket impl pattern: concrete type + reference delegation
#[batch_impl(i32 #to_i32{*self}, <T: ToI32> &T #delegate(to_i32){**self})]
trait ToI32 { fn to_i32(&self) -> i32; }
// → impl<T: ToI32> ToI32 for &T { fn to_i32(&self) -> i32 { (**self).to_i32() } }Delegation limits:
#delegateonly supports methods (const / type items error); its arguments support onlyselfand plain identifiers (pattern parameters such as(a, b)cannot be forwarded and error). The remaining limits are the same as blanket delegation —*const/*mut,self, and empty lists all raisecompile_error!.
§Combining directives with the DSL
Directives can be freely chained with operators and {body} suffixes:
#[batch_impl(
usize #name{"usize"} { fn kind(&self) -> &str { "number" } }
)]
trait Tagged { fn name(&self) -> &str; fn kind(&self) -> &str; }
#[batch_impl(<T: std::fmt::Display> Vec<T> #t10{self.len()})]
trait Len { fn t10(&self) -> usize; }§Extension mechanism (open directive system)
An unrecognized #name(args){body} is automatically converted into a {...} code block whose content is a functional macro invocation
name!{(args){body} trait ...} — the method-name list, body, and the whole trait definition are handed to the user’s same-named macro,
which expands them into the needed fn definitions. This means the directive system is open and
shares the same origin as #fill / #delegate: both are “read the trait → generate fn definitions”, except the implementation is delegated to
the user (#fill is the library implementation, an open directive is a user-macro implementation).
#[batch_impl(usize #batch_preprocess_test(add,inc){*self+1})]
trait AddInc {
fn add(&self) -> Self;
fn inc(&self) -> Self;
}
// → trait AddInc { fn add(&self) -> Self; fn inc(&self) -> Self; }
// → impl AddInc for usize {
// batch_preprocess_test!{(add,inc){*self+1} trait AddInc { fn add(&self) -> Self; fn inc(&self) -> Self; }}
// }
// → the macro expands to: fn add(&self) -> Self { *self + 1 } fn inc(&self) -> Self { *self + 1 }Note: this is a “user-defined
#fill” — each type can attach its own (usize #batch_preprocess_test(...){...}, isize #batch_preprocess_test(...){...}), and the trait definition still comes only from the trait output by#[batch_impl], without duplication.
§#blanket(methods){wrapper list} — blanket delegation
Generates delegating impls in bulk for wrapper types: every element in {wrapper list} may be any type expression
(& / &mut / Box / Rc / Arc / MyPtr / Box^Arc / Cow<'_>…), each producing a complete delegation spec. First implement the trait for the inner type, then blanket-cover the wrappers:
#[batch_impl(u32 { fn name(&self) -> String { self.to_string() } })]
#[batch_impl(#blanket(@all){&, Box, Rc})]
trait Name {
fn name(&self) -> String;
}
// → impl Name for u32 { ... } // first batch_impl
// → impl<T> Name for &T where T: Name { fn name(&self) -> String { (**self).name() } }
// → impl<T> Name for Box<T> where T: Name { ... } // blanket: one delegated body per wrapper
// → impl<T> Name for Rc<T> where T: Name { ... }Nested wrappers use ^ chains (target type = wrapper expression ^T, where T is a fresh generic); < prefill is append semantics (Box<Arc>^T = Box<Arc, T>, wrong):
Box^Arc:2 → Box<Arc<T>>; Cow<'_> → Cow<'_, T>.
Deref depth of the delegating body: 1 by default (**self); nesting requires an explicit :N (the number of *s =
N + 1, e.g. Box^Arc:2 → ***self). The macro never guesses the Deref depth inside a wrapper — forgetting
:N on a nested wrapper degrades into a rustc method-not-found error.
#[batch_impl(u32 { fn deep(&self) -> u32 { *self } })]
#[batch_impl(#blanket(deep){Box^Rc:2, Box^Box^Box:3})]
trait Deep {
fn deep(&self) -> u32;
}methods is the same as for #delegate (@all / @all_methods / an explicit method-name list).
Wrapper constraint predicates: a wrapper element may end with where{...} (after :N); the predicates join
the impl’s where clause — this handles wrappers whose deref target ≠ T (e.g. Cow<'_, T>’s deref target is T::Owned,
so a blanket default-delegating to T needs the extra constraints). In the predicates,
@0 refers to the target generic (fresh T) and @trait refers to the local trait name; the built-in @Cow constant
is the packaged Cow<'_> + its intrinsic constraints:
#[batch_impl(#blanket(@all_methods){Cow<'_> where{@0: ToOwned + ?Sized, @0::Owned: @trait}})]
trait CowName { fn len(&self) -> usize; }
// → impl<T> CowName for Cow<'_, T>
// where T: CowName, T: ToOwned + ?Sized, T::Owned: CowName
// equivalent form (built-in constant):
#[batch_impl(#blanket(@all_methods){@Cow})]
trait CowName2 { fn len(&self) -> usize; }Generic trait support (trait Foo<X: Clone>): trait parameters are copied verbatim as impl generics
(impl<X: Clone, T: Foo<X>> Foo<X> for wrapper<T> where ...); trait-level where predicates pass through.
Assoc type / const delegation: when @all includes const/type items, projections are generated —
type Item = <T as Foo<X>>::Item; / const N: Ty = <T as Foo<X>>::N; — so traits with required associated types can also be blanket-covered.
#[batch_impl(Foo<u32> u32 {
type Item = u8;
fn m(&self) -> u32 { *self }
})]
#[batch_impl(#blanket(@all){&, Box})]
trait Foo<X: Clone> {
type Item;
fn m(&self) -> X;
}
// → impl<X: Clone, T> Foo<X> for Box<T> where T: Foo<X> {
// type Item = <T as Foo<X>>::Item;
// fn m(&self) -> X { (**self).m() }
// }Constraints: *const/*mut (safe code cannot dereference raw pointers to delegate), self (meaningless), and empty elements / illegal :N all error — write #delegate by hand instead. by-value receiver methods
(fn consume(self)) have delegation semantics that depend on the wrapper’s Deref/move capability, which cannot be told apart at macro expansion time — everything is allowed through and rustc has the final say.
Static-method delegation (new in 0.6.2): methods without a receiver (associated functions in
@all_static_methods / @all_methods) are forwarded through the blanket generic t — the delegating body is
t::make(...) instead of a deref chain (static methods have no self to dereference). Direct calls, nested
wrappers (Box<Box<u8>>), and argument forwarding all reach the underlying impl through the t: Trait bound —
the same forwarding semantics as the <t as Trait>::Item projection for assoc items:
#[batch_impl(#blanket(@all_static_methods){Box})]
trait StaticT {
fn make() -> u8;
fn pair(a: u8, b: u8) -> u16;
}
impl StaticT for u8 {
fn make() -> u8 { 7 }
fn pair(a: u8, b: u8) -> u16 { (a as u16) * 10 + b as u16 }
}
// → impl<T> StaticT for Box<T> where T: StaticT {
// fn make() -> u8 { T::make() }
// fn pair(a: u8, b: u8) -> u16 { T::pair(a, b) }
// }
// calls: <Box<u8> as StaticT>::make() → T::make() → u8::make() → 7
// <Box<Box<u8>> as StaticT>::make() → recursive delegation (Box<u8>: StaticT bound)§8. where Clauses
§The where{...} suffix
The where{...} suffix follows the target type and holds pass-through where predicates; several merge together:
#[batch_impl(<T: Clone> Sortable<T> Vec<T> where{ T: Ord } {
fn sort(&self) -> Vec<T> { let mut v = self.clone(); v.sort(); v }
})]
trait Sortable<T> { fn sort(&self) -> Vec<T>; }
// → impl<T: Clone> Sortable<T> for Vec<T> where T: Ord { ... }
#[batch_impl(<A> <B> PairAB<A, B> (A, B) where{A: Clone} where{B: Clone} {
fn pair(&self) -> (A, B) { (self.0.clone(), self.1.clone()) }
})]
trait PairAB<A, B> { fn pair(&self) -> (A, B); }§Bare where predicates {code block}
Rust-style bare writing is also supported (common to all three interfaces); the {...} code block after the predicates must exist;
the predicate region ends at the first {...} code block (ident!{...} macro-call bodies and code blocks inside <N = {5}> angle brackets don’t count), and comma-separated predicates are not split across specs:
#[batch_impl(<A> <B> PairAB<A, B> (A, B) where A: Clone, B: Clone {
fn pair(&self) -> (A, B) { (self.0.clone(), self.1.clone()) }
})]
trait PairAB<A, B> { fn pair(&self) -> (A, B); }
// → impl<A, B> PairAB<A, B> for (A, B) where A: Clone, B: Clone { ... }Multiple where segments can be written in sequence (where A: Clone where B: Clone), equivalent to the older multiple where{...} form.
§9. fn Types
#[batch_impl(fn^(i32, u32))]
trait FnSimple {}
// fn type with an appended return type
#[batch_impl(fn(i32, u32)-String)]
trait FnWithReturn {}
// fn types generated in bulk (Cartesian product)
#[batch_impl(fn-(i32, u32)^2)]
trait FnTupleGen {}
// → impl FnTupleGen for fn(i32, i32) {}
// → impl FnTupleGen for fn(i32, u32) {}
// → impl FnTupleGen for fn(u32, i32) {}
// → impl FnTupleGen for fn(u32, u32) {}unsafe fn(...) types: when unsafe immediately precedes fn, it modifies the fn type itself, unrelated to the
unsafe impl marker of unsafe^T (unsafe^fn(...) is “unsafe impl targeting an fn type”):
#[batch_impl(unsafe fn(i32, u32) -> u32)]
trait UnsafeFnType {}
// → impl UnsafeFnType for unsafe fn(i32, u32) -> u32 {}
#[batch_impl(unsafe fn^(i32, u32) - i64)]
trait UnsafeFnType2 {}
// → impl UnsafeFnType2 for unsafe fn(i32, u32) -> i64 {}
unsafedisambiguation rules: a bareunsafe(followed by^/-or standing alone) = unsafe impl marker;unsafe fn...= an unsafe fn type;unsafe <other type>(juxtaposed, no operator) = error (almost certainly a typo that forgot^; writeunsafe^T).
§10. The Full Modifier Reference
| Modifier | Meaning |
|---|---|
& | reference (&^T → &T) |
&mut | mutable reference (&mut^T → &mut T) |
*const | raw pointer (*const^T → *const T) |
*mut | mutable raw pointer (*mut^T → *mut T) |
self | identity (self^T → T) |
unsafe | bare unsafe^T = unsafe impl marker |
#[attr] | attribute prefix (#[attr]^T → attribute prepended to the impl) |
[] | empty base ([]^T → [T], []-T-N → [T; N]) |
[T] | slice ([T]^N → fixed-size array [T; N]) |
#[batch_impl(unsafe^usize, isize)]
unsafe trait UnsafePartial {}
// all impls of an unsafe trait are automatically unsafe
#[batch_impl(*const^u32, *mut^i32)]
trait PtrMarker {}
#[batch_impl(*const^Box^u32)]
trait ConstPtrChain {}
// → impl ConstPtrChain for *const Box<u32> {}
#[batch_impl(#[allow(dead_code)]^usize, isize)]
trait AttrSimple {}A prefix acting on a whole list is automatically distributed to each item (#[attr] [u8, u16] and
& [u8, u16] both expand to one impl per item, each carrying the prefix/modifier).
§Array/slice builders
#[batch_impl([]^u8)] // → impl ArrSlice for [u8] {}
trait ArrSlice {}
#[batch_impl([u8]^3)] // → impl ArrLit for [u8; 3] {}
trait ArrLit {}
#[batch_impl(<const N: usize> [u8]^N)] // → impl<const N: usize> ArrConst for [u8; N] {}
trait ArrConst {}
#[batch_impl([u8]^1..3)] // → impl ArrRange for [u8; 1] {} and [u8; 2] {}
trait ArrRange {}§Complex-type pass-through
Unrecognized types pass through verbatim:
#[batch_impl(
(i32, String),
&str,
Box<dyn std::fmt::Display>,
fn(i32) -> bool,
dyn Fn() + Send + Sync
)]
trait ComplexMarker {}§11. Tuple Generation and Matrices
When the right side of ^ is a number or a range, tuples of the specified lengths are generated (numbers are only used as exponents):
| Syntax | Expands to |
|---|---|
()^3 | (A, B, C) (with 3 generic parameters) |
(T,)^3 | (T, T, T) |
(<Clone>)^3 | (A:Clone, B:Clone, C:Clone) |
(T1, T2)^2 | Cartesian product (T1,T1), (T1,T2), (T2,T1), (T2,T2) |
()^1..3 | (A,), (A, B) (lengths 1 to 2) |
()^1..=3 | (A,), (A, B), (A, B, C) (lengths 1 to 3) |
(T,)^2..4 | (T, T), (T, T, T) (lengths 2 to 3) |
Note:
(T)is grouping (not a tuple);(T,)is the single-element tuple.
#[batch_impl(()^1..=4 { fn describe(&self) -> &'static str { "tuple" } })]
trait DescribeTuple { fn describe(&self) -> &'static str; }
// → 4 impls: (A,), (A, B), (A, B, C), (A, B, C, D)§Wrapping the whole matrix in const-generic fixed-size arrays
[] as the base of a - accumulation chain:
#[batch_impl(
<const N: usize> []-[&, self, Box]^[u8, i8, ()^0..3]-N
)]
trait FixedMatrix {}
// → impl<const N: usize> FixedMatrix for [&u8; N] { }
// → impl<const N: usize> FixedMatrix for [Box<i8>; N] { }
// → impl<const N: usize, A> FixedMatrix for [(A,); N] { } // tuple fresh generics are hoisted automatically
// → ...§@ Constants — built-in type-family names
Common type matrices don’t have to be written by hand: @ constants expand to literal lists during preprocessing, equivalent to writing them out.
| Constant | Expands to |
|---|---|
@uint | [u8, u16, u32, u64, u128, usize] |
@int | [i8, i16, i32, i64, i128, isize] |
@float | [f32, f64] |
@num | @uint + @int + @float (14) |
@scalar | @num + [bool, char] (16) |
@u8..u128 | [u8, u16, u32, u64, u128] (endpoints inclusive; @i8..i128 / @f32..f64 work the same) |
#[batch_impl(@scalar)]
trait ScalarTrait {}
// → 16 impls: one each for u8..charAll three entry points (#[batch_impl] / #[batch_impl_only] / batch_trait!) support the built-in
constants. batch_trait! additionally supports custom constants: leading @name=value; sections in the macro arguments define them,
and later sections reuse them. Values are arbitrary tokens (lazily expanded — stored verbatim, and recursively expanded after concatenation at the point of reference), so values can directly contain DSL operations and chain references to other constants:
trait TraitA {}
trait TraitB {}
batch_trait!(
@nums=[u8, u16, u32];
@uints=@uint; // references a built-in constant
@wrapped=[Box, Rc]^@nums; // value contains DSL operations (evaluated at the reference site)
@chain=@wrapped; // chained reference to a user constant
TraitA: @chain;
TraitB: [Box, Rc]^@uints;
);Reference visibility: inside a constant definition you may only reference built-in constants or user constants already defined before it —
circular references (@a=@a) and forward references (@a=@b where @b is defined later) error at the definition site.
Unknown @xxx, illegal range endpoints, custom constants colliding with built-ins, and circular/forward references all raise compile_error!.
§Section-level @trait in batch_trait! (reusing “generic declarations + trait name” across sections)
In batch_trait!’s multiple sections, each section has a different trait name — the @trait inside constant values is replaced per section with that section’s trait path after sectioning:
batch_trait!{
@type_t = <T> @trait <T>; // packs "generic declaration + this segment's trait name"
A: @type_t [&, Box]^T; // → <T> A<T> [&, Box]^T
B: @type_t Box^[T, Vec<T>]; // → <T> B<T> Box^[T, Vec<T>]
}§Completing the macro-meta layer: @trait / @all family / @Cow / @0
batch_impl / batch_impl_only hold the trait definition, and the macro-meta layer additionally provides trait-aware
constants (batch_trait! is a function-like macro that can’t get the definition, so it errors on the markers below):
| Marker | Expands to | Use case |
|---|---|---|
@trait | the trait’s full path (batch_impl = local name, batch_impl_only = external path); in batch_trait! it is section-level: expands to that section’s trait path | blanket wrapper where predicates; batch_trait! packing “generic declarations + trait name” across sections; the trait-name part of a top-level spec (<T> @trait<T> Vec<T>) |
@all / @all_methods / @all_constants / @all_types | [item names, ...] (Bracket group) | directive scope selection — #fill(@all) is equivalent to the old #fill(#all) |
@all_required* / @all_default* | Bracket groups filtered by default-implementation state | fill only the required / override only the defaulted |
@all_ref_methods / @all_value_methods / @all_static_methods | Bracket groups filtered by receiver kind (&self/&mut self / self / associated functions) | delegate only reference methods (bypassing the uncertain by-value delegation semantics); #blanket(@all_ref_methods){Box} |
@Cow | Cow<'_> + intrinsic constraint predicates | blanket wrapping (deref target = T::Owned) |
@N (positional reference) | the name of the Nth generic inside where predicates | in blanket wrapper predicates @0 = the target generic (fresh T); in tuple generation ()^N, @k = the kth fresh generic; with user generics, @k = the kth impl generic |
After the @all family expands into Bracket groups, normal directive-argument parsing applies: # is no longer a scope marker —
# now only appears in the single form of a directive name (#fill/#delegate/#blanket/open extensions), and scope
selection is uniformly owned by the macro-meta layer. Subtraction is unaffected: #fill(@all, -foo), #fill(@all, -[a,b]).
Directive arguments support [a, b] lists: #fill([m1, m2]){...}; - exclusions can also be written
-[a, b] (the @all expansion already has this shape, and hand-writing it is equivalent).
@N positional references in where predicates — the generic names the macro generates are unknown to the user
(fresh names); constrain them by position:
// tuple-generated fresh generics: @0 = the 0th, @1 = the 1st
#[batch_impl(()^2 where{@0: Clone, @1: Copy} { fn tmk() -> u32 { 2 } })]
trait TupleWhereAt { fn tmk() -> u32; }
// → impl<A: Clone, B: Copy> TupleWhereAt for (A, B) { fn tmk() -> u32 { 2 } }
// user generics: @0 = the 0th impl generic
#[batch_impl(<T> AtWhere<T> Vec<T> where{@0: Default} { fn an(&self) -> usize { self.len() } })]
trait AtWhere<T: Clone> { fn an(&self) -> usize; }
// → impl<T: Clone + Default> AtWhere<T> for Vec<T> { ... }(In blanket wrapper predicates @0 = the target generic, fresh T — see §7 #blanket; @trait can also appear
in ordinary where predicates, e.g. where{@0: @trait<T>}.)
§12. Three Entry Points
| Macro | Purpose |
|---|---|
#[batch_impl] | attribute macro annotated on a trait definition; the macro arguments are the DSL |
#[batch_impl_only] | same, but discards the trait definition and only outputs impl blocks |
batch_trait! | function-like macro that generates impls in bulk for already-declared traits (supports multiple traits) |
All three accept the same DSL arguments.
§#[batch_impl_only]
For cases where the trait is already defined elsewhere and you only need bulk impls. The trait definition still has to be written (it is only read for method signatures), but the output does not include the trait:
#[batch_impl_only(usize #hello{"hi"})]
trait Greet { fn hello(&self) -> &str; } // this dummy definition is discarded
// → impl Greet for usize { fn hello(&self) -> &str { "hi" } }A #path::to::Trait: path prefix is supported, generating impls for traits defined in external modules
(the trailing identifier of the path must match the local dummy trait name; #[batch_impl] does not support this prefix):
#[batch_impl_only(#ext::traits::TraitName: usize, isize)]
trait TraitName { }
// → impl ext::traits::TraitName for usize {}
// → impl ext::traits::TraitName for isize {}§batch_trait!
Generates impls in bulk for already-declared traits; ; separates multiple trait sections. Syntax:
[unsafe] TraitPath: impl-specs; the right side of : accepts the type DSL and @ constants (the same
type syntax as #[batch_impl]), and additionally supports multiple trait sections, path traits (e.g.
foo::C), and unsafe sections:
use batch_impl::batch_trait;
trait A {}
trait B<T> {}
unsafe trait UnsafeTrait {}
batch_trait!(
A: usize, isize;
B: <T> B<T> Vec<T>;
unsafe UnsafeTrait: usize
);Limitation:
batch_trait!does not support#directives (#fill/#delegate/#blanket/ open extensions) — directives need the trait definition as the signature source of truth, whilebatch_trait!is a function-like macro that can’t get the trait definition. When you need directives, use#[batch_impl]/#[batch_impl_only]instead.
§13. Error Messages
All DSL syntax errors are reported through compile_error!() with English messages (since 0.6.2), pointing as
precisely as possible at the offending token in the source (span diagnostics; tokens inside groups and the Err
return path show the macro invocation line), and never panic:
| Bad input | Error message (excerpt) |
|---|---|
batch_trait!(;) | batch_trait! expects a trait name |
batch_trait!(A) | batch_trait! expects ':' to separate the trait name and impl-specs |
batch_trait!(A: B::) | batch_trait! expects an ident as the trait name |
A^ (missing right operand) | batch-impl: missing operand after '^' (e.g. 'T^U') — points at the ^ itself |
A,,B | batch-impl: missing operand between consecutive commas ',,’` |
3..2 (empty range) | batch-impl: range '3..2' is empty (start not below end); no impls will be generated |
^2000 (over the limit) | batch-impl: tuple '^2000' expands to 2000 items (limit 1024); likely exponential/range/Cartesian typo |
| nesting depth over 128 | batch-impl: nesting depth exceeds 128 levels (perhaps an accidental extra bracket) |
@unknown | batch-impl: unknown @ constant '@unknown'; built-ins: '@uint' ... |
@u32..u8 (endpoints reversed) | batch-impl: range start is greater than end: 'u32..u8' |
@a=@a (circular reference) | batch-impl: constant '@a' references unknown '@a' (undefined or defined later; ...) |
#fill() (empty arguments) | batch-impl: the directive's argument list cannot be empty |
missing target after - | batch-impl: directive arguments cannot be empty |
bare where without a code block | batch-impl: \where` predicates are missing a code block {…}` |
| inherited predicate referring to an undeclared parameter | batch-impl: trait argument 'X' maps to parameter 'T' (bound 'IntoIterator'); automatic inheritance requires the same name; rename to 'T' or write the bound manually |
#blanket illegal wrapper | batch-impl: #blanket ... (*const/*mut, self, empty elements, illegal :N, and non-forwardable pattern parameters all error) |
Macros§
- batch_
trait - Function-like macro that generates
implblocks for a declared trait in batch.
Attribute Macros§
- batch_
impl - Attribute macro that generates
implblocks for a trait in batch. - batch_
impl_ only - Same as
#[batch_impl], but discards the annotated trait definition and only emitsimplblocks.