Expand description
§batch-impl
v0.8.2 (2026-08-19) — variadic segments and repeat blocks: impl{...} templates declare a variadic segment with ident@.. (covers every remaining tuple position, names aligned with the leaf position) and bodies repeat with @(...).. (@ident name references, @N index cursors, nested blocks are Cartesian) — one alga2-style spec covers every tuple arity (()^1..=4 where{@all_fresh: Magma} impl{(A@..,)} #combine{...} → impl<A0..An> Magma for (A0, ..., An) where A0: Magma, ...); where predicates now resolve @N inside angle groups too (Module<..., Scalar = @0::Scalar> — associated-type value references) and gain the @N.. open range (“from the second element on”, empty when past the end) — the alga2 tuple-Module scalar-equality constraint;
v0.8.1 — 0.8.1 released: where{...} predicate groups are angle-paired — a two-arg bound inside a where{...} block (@all_fresh: Semiring<Additive, Multiplicative>) no longer splits at its depth-0 comma into a bad predicate (found in real use by alga2; code bodies stay passthrough);
v0.8.0 (2026-08-18) — 0.8.0 released: style and docs groundwork (rustfmt width caps dropped, example comments translated to English, architecture test counts refreshed) + flat-chain depth guards (^/- chains, attachment chains, and chained type segments capped at 128 levels) + the 0.7.2 attribute-macro custom @ constants feature is reverted (custom @name=value; sections are batch_trait!-only again; write attribute-macro matrices directly with ^/-/*) + Ext 2 impl{...} Self-part shape templates: bind the generated impl’s target shape with a standard Rust template — an ident equal to the target’s at that position is kept, a different one is rewritten in the target/where/body (Box<u32> impl{Rc<T>} → Rc := Box, T := u32); template matching covers every syn::Type form (slices/tuples/fixed arrays/references/pointers/paths), fixed-array lengths and '_' lifetime wildcards bind — write one prototype impl per shape family and cover a whole matrix ([Box,Rc]^@num impl{Box<u8>} #max{...}; Cow<'_, @num> impl{Cow<'_, u8>} for lifetime-bearing families) + Ext 1 ItemImpl entry: #[batch_impl] also accepts an impl block and batch-instantiates it from a shape-template × matrix-source (A<B> : [Box,Rc]^[usize,isize] → 4 impls, slots rewritten in for-Type/where/body);
v0.7.2 (2026-08-14) — 0.7.2 released: user-language diagnostics (no reserved-name leaks), batch_preview! expansion preview, generator-splat declaration hoisting in trait args, #blanket by-value receiver fix, open-extension protocol convergence, syntax-freeze commitment, attribute-macro custom @ constants (reverted in 0.8.0); 0.7.1 released: targeted diagnostics for stray ;/=/@/#, adjacent types, empty bindings/bounds and typo suggestions (no more raw rustc errors); 0.7.0: the splat * prefix (flatten containers/generators into lists, *[...] distribute / *(...) append as left operand), array distribution propagation (nested [A,B] Cartesian products), generator fresh-declaration fix, splat power inside generic args (Frac<*(*@u*)^2> = 36 impls), concrete-type args reject bindings/bounds, #fill single-item preference (#name{...}).
A procedural macro crate that batch-generates impl blocks for Rust traits — one line of DSL, expanded into N impls.
Beyond the core batch-impl DSL, the crate carries two deeper layers: a
macro-meta layer (@ constants / selectors / positional references — a
small meta-language for composing generated generics) and an open directive
system (#fill / #delegate / #blanket + user #name macros, including
top-level macro injection {! ...}). Think of it as a batch impl generator
with a pluggable codegen protocol — the “one line” story covers the common
case; the layers below it cover the composing cases (dispatch matrices,
blanket delegation, custom codegen).
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) |
*[...] / *(...) | splat: flatten into the enclosing list | [a, *[b,c]] = [a,b,c]; left *[...] distributes / *(...) appends |
#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.8.1"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 | §3 |
Splat * prefix | Flatten containers/generators into the enclosing list — in-list splice, ^ right-operand flat append, generic multi-arg; left operand *[...] distribute / *(...) append | §4 |
^ / - operators | Right/left associativity of the same operation: nesting vs. accumulation | §2 |
| Generic automation | A<> copied as-is, same-name inheritance, trait where-clause inheritance | §5 |
| Associated type bindings | Iter<Item=T> → type Item = T; | §5.3 |
Directive system #name/#fill/#delegate | Auto-copy signatures, batch-fill bodies, delegate calls | §7 |
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) | §7 |
| Open extension | Unknown #name(args){body} becomes a top-level macro call: your same-named macro receives {spec}(args){body}trait and emits its own impl | §7 |
@ constants | Built-in families @u*/@scalar/@u8..u128 + @trait/@all family/@Cow + batch_trait! leading @name=value; custom sections (lazy expansion, chained references; attribute macros do not support them — write matrices directly) | §6 |
| Generic parameter families | @all_type_params / @all_const_params / @all_lifetimes — generic declarations copy the trait’s formal params (bounds via same-name inheritance) | §6 |
Unified macro-meta layer @ | # keeps only directive names; scope selection (@all family, incl. required/default and receiver filters) and positional references (@N, @g_i, @all_fresh, @N..=M) belong to the macro-meta layer | §6 |
where{...} | Unified constraint container (<> keeps only names), blanket constraints merged side by side | §8 |
| Tuple generation | ()^3, (T,)^N, Cartesian product, ranges | §9 |
| Variadic segments + repeat blocks | ident@.. in impl{...} templates (cover every remaining tuple position) + @(...).. body repetition (@ident names, @N index cursors) — one spec covers every tuple arity | §8.4 |
| fn types / unsafe / pointers / attributes | Full support for type-level modifiers | §10 |
Shorthand: a single method
#fill([foo]){body}equals#foo{body}; predicates + code blockwhere{predicates} {code block}can be written bare aswhere predicates {code block}(see §7.2 / §8.2).
§Syntax-freeze commitment (0.7.2)
The semantics of every existing token are final — ^/-, []/()/<>, where, the # directives, the @ constants, and the splat will not change behavior again. Future releases only add (new directives / constants / tools), refine diagnostics, and polish docs; any change to existing semantics is a deliberate breaking release (the @N stability commitment, now extended to the whole surface). @g_i / @all_fresh / @N..M are power-user tier (tutorial §6.4) — start from @u* / @all_methods / @0.
§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) - Ext 1 / Ext 2 (0.8.0): the ItemImpl entry —
#[batch_impl]also accepts animplblock and batch-instantiates it from a shape-template × matrix-source (tutorial §8.5); theimpl{...}Self-part shape templates — bind the generated impl’s target shape and write one prototype impl per shape family to cover a whole matrix, incl. lifetime-bearing families likeCow(tutorial §8.4) - Variadic segments + repeat blocks (0.8.2):
ident@..template segments and@(...)..body repetition — the alga2-style()^1..=4 where{@all_fresh: Magma} impl{(A@..,)} #combine{...}covers every tuple arity with one spec (tutorial §8.4) - Expansion preview:
batch_preview!(wrap the#[batch_impl(...)] trait/#[batch_impl(...)] implinput and read the real expansion, plus^/-associativity miswrite notes) - Examples:
examples/quickstart.rs(feature demo),examples/simplify.rs(a real scenario with 29 impls ≈ 15 lines of DSL),examples/typeclass.rs(type-class style: aNum/UNum/INum/FNumhierarchy + 36From<bool>impls forFrac<T, U>) - Developers: internal architecture in
docs/architecture.md, development changelog indocs/dev-changelog.md
§License
MIT OR Apache-2.0
§batch-impl Tutorial
v0.8.2 (2026-08-19) — variadic segments (ident@..) in impl{...} templates and repeat blocks (@(...)..) in bodies, see §8.4;
v0.8.1 — the where{...} angle-pairing hotfix (see the CHANGELOG);
v0.7.2 — 0.7.2 adds the batch_preview! expansion preview, generator-splat declaration hoisting in trait args, #blanket by-value receiver forwarding, custom @ constant sections for the attribute macros (reverted in 0.8.0), and user-language @ diagnostics; 0.7.1 adds targeted diagnostics (stray/adjacent/empty tokens, typo suggestions) instead of raw rustc errors; 0.7.0 adds the * flatten operator on top of the existing skeleton, and upgrades <>/()/[] from “passive syntax” to “programmable structures”: generic-argument positions now accept generators (()^N), splats (*(A,B)), constant families (@u*), lists ([A,B]), bindings (Item=u32) and nested types.
Progressive DSL learning: from a one-line impl to advanced matrix combinations. All examples are compilable code (the code blocks of this English tutorial double as doctests), and every step’s output is plain Rust — the generated impls are token-equivalent to handwritten ones.
§0. Three systems + one operator
Every capability of batch-impl is built from three pillars (polished continuously from 0.0 to 0.6) plus one operator (0.7.0):
| Part | Notation | Role |
|---|---|---|
| apply system | ^ / - / [] / () | Type matrix: apply the left container/modifier to the right type, lists expand into multiple impls |
| directive system | #name / #fill / #delegate / #blanket | Copy signatures from the trait definition, fill bodies in bulk, delegate calls, blanket delegation |
| constant system | @u* / @scalar / @u8..u128 / @name=... | Macro-meta layer: name and reuse type-matrix entries, pure lexical substitution |
* operator | *[...] / *(...) | Flatten: splice a container/generator into the enclosing list — new in 0.7.0, effective in every position |
Preprocessing order (fixed four-stage pipeline): @ constant expansion → <> angle-bracket pairing → # directive expansion → where processing. The order decides what you can write into what: @ results may contain <> (paired afterwards), # arguments may reference @-expanded lists, where sees the complete structure last.
§1. Starting from a One-Line impl
#[batch_impl(...)] annotates a trait definition; every spec in its argument 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 spec skeleton:
<impl-generics> TraitName<trait-generics> TargetType { body }?| Part | Example | When needed |
|---|---|---|
<impl-generics> | <T>, <T: Clone>, <const N: usize> | when the impl block needs generic params |
TraitName<trait-generics> | MyTrait<T>, MyTrait<Vec<T>> | when the trait definition has generic params |
| Target type | usize, Vec<T>, &str | required |
{ body } | { fn m(&self) -> usize { 0 } } | when a custom body is needed |
Multiple specs are separated by ,: #[batch_impl(usize, isize)].
§2. Type Matrix: ^ and -
^ and - are the same operation: the left side is a modifier/container, the right side the target type. They differ only in associativity: ^ is right-associative (nesting), - is left-associative (accumulating params).
Precedence from low to high: ; < , < - < ^; () grouping sits above all operators.
| Writing | Expansion |
|---|---|
Box^T | Box<T> |
Box^<X,Y> | Box<X, Y> (multi-param container) |
Box^Box^T | Box<Box<T>> (right-associative nesting) |
HashMap<K>^V | HashMap<K, V> (prefilled generics appended) |
&^Box^T | &Box<T> (chained modifiers) |
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 |
[HashMap<K>, Vec<K>]^V | HashMap<K, V>, Vec<K, V> |
Note:
Box^Vec-u32is wrong (it parses asBox<Vec, u32>); writeBox^Vec^u32instead. When you miswrite it, rustc’s E0107 error prints the renderedBox<Vec, u32>verbatim — the mistake is self-evident.
Operand strictness: both sides of
^/-/,must have operands —A^,^A,-A,,A,A,,Ball reportcompile_error!; only trailing commas (A,/[A, B,]) are allowed, and()/[]brackets are real tokens, not empty operands.;stays lenient as abatch_trait!section boundary.
#[batch_impl(Box^Vec^u32, HashMap<u8>^String)]
trait T {}
// → impl T for Box<Vec<u32>> {}
// → impl T for HashMap<u8, String> {}§3. 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 { ... }Distribution propagation: [A, B] lists are distribution sources — beyond being targets/operands, nested positions propagate too:
#[batch_impl((u8, [u16, u32, u64]))]
trait T {}
// → impl T for (u8, u16) {}
// → impl T for (u8, u32) {}
// → impl T for (u8, u64) {}
#[batch_impl(Vec<[u8, u16, u32]>)]
trait V {}
// → impl V for Vec<u8> {}
// → impl V for Vec<u16> {}
// → impl V for Vec<u32> {}Rule: [A, B] inside a tuple/generic-arg position → Cartesian-product distribution (all combinations of multiple arrays); nested arrays recurse to leaves (Vec<[[A,B], C]> → Vec<A>/Vec<B>/Vec<C>); combos of (X, [A,B])^N containing arrays are covered by the outer distribution. Note: concrete generators combined with fresh generators may overlap (E0119 — same fresh count/structure); rustc catches it — use generators with different fresh counts to avoid.
§Independent/shared body merging
List items may carry independent bodies, merged with the shared body — different items coexist (writing the same item twice is a user error rustc reports):
#[batch_impl([
usize { fn name(&self) -> &'static str { "usize" } },
isize { fn name(&self) -> &'static str { "isize" } },
f32 { fn name(&self) -> &'static str { "f32" } },
] {
fn zero() -> Self { Default::default() }
})]
trait Tagged { fn zero() -> Self; fn name(&self) -> &'static str; }
// → impl Tagged for usize { fn name... "usize"; fn zero() { Default::default() } }(independent name + shared zero)
// → impl Tagged for isize { fn name... "isize"; fn zero() { 0 } }
// → impl Tagged for f32 { fn name... "f32"; fn zero() { 0 } }§4. splat * — the Flatten Operator (the protagonist of 0.7.0)
The splat draws its intuition from Python’s * unpacking — [a, *b] splices a list, f(*args) unfolds arguments. batch-impl’s * is the same single-layer unpack: a splat splices a container/generator into the enclosing list, expanding exactly one level.
| Python | batch-impl |
|---|---|
[a, *b] | [A, *[B, C]] — splice a list into the outer list |
f(*args) | T-*(A, B, C) — unfold a generator into argument positions |
| one level of unpack | *((a,b),) = one (a,b) impl (tuples stay intact) |
Motivation: * compresses a nested generator into a multi-arg container. Instead of hand-writing T-[A,B,C]-[A,B,C]-[A,B,C] (27 combos of nested lists), one line gives the same 27 impls:
struct T<A, B, C>(A, B, C); // 3-arg container
struct A; struct B; struct C;
#[batch_impl(T-*(A, B, C)^3)] // splat-pow: unfold (A,B,C)^3 into three arg positions
trait Matrix27 {}
// → 27 impls: T<A,A,A> / T<A,A,B> / ... / T<C,C,C>(same as T-[A,B,C]-[A,B,C]-[A,B,C])*[...] / *(...) splices a container/generator into the enclosing list. A splat stays a whole unit through parse/apply/expand and only flattens into its elements at codegen — one code path for every position.
§4.1 In-list / in-tuple splicing
#[batch_impl([A, *[B, C]])]
trait T {}
// → impl T for A {} / B / C(splice: `[A, *[B, C]]` = `[A, B, C]`)
#[batch_impl((A, *(B, C)))]
trait U {}
// → impl U for (A, B, C) {}(tuple splice appends)§4.2 Left operand: distribute vs append
[] is a set and () is a sequence — splat just mirrors the source bracket, so *[A,B]^T distributes (each element applies T, keeping set semantics) and *(A,B)^T appends (keeping list semantics). This is not a new rule; it preserves the underlying container’s behavior, and TySplat::Array/TySplat::Tuple mirror TyArray/TyTuple.
#[batch_impl(*[Vec, Box]^u8)] // array splat distributes: each element ^u8
trait T1 {}
// → impl T1 for Vec<u8> {} / Box<u8>
#[batch_impl(*(Vec<u8>, Box<u8>)^u16)] // tuple splat appends: the right operand joins
trait T2 {}
// → impl T2 for Vec<u8> {} / Box<u8> / u16(append)§4.3 Generic args and trait paths
struct Pair<X, Y>(X, Y);
struct A; struct B;
#[batch_impl(Pair<*(A, B)>)]
trait G1 {}
// → impl G1 for Pair<A, B> {}(one impl, two args)
#[batch_impl(Conv<*(A, B)> Pair<A, B> #cv{unimplemented!()})]
trait Conv<T, U>: Sized { fn cv(_v: T, _o: U) -> Self; }
// → impl Conv<A, B> for Pair<A, B> { fn cv(_v: A, _o: B) -> Self { unimplemented!() } }A splat power inside generic args distributes its Cartesian result one impl per pair:
struct Frac<T, U>(T, U);
#[batch_impl(Frac<*(*@u*)^2>)]
trait Pow {}
// → impl Pow for Frac<u8, u8> {} ... impl Pow for Frac<usize, usize> {}(36 impls)§4.4 Container rule
A group whose content is a lone splat parses as the container holding the splat as one element — (*(a,b)) = ( *(a,b) ), [*(a,b)] = [ *(a,b) ]; the splat element expands only in codegen.
§4.5 Generator re-wrap
*(()^N) — a generator splat — hoists fresh declarations and splats the tuple into a container:
struct Pair3<A, B>(A, B);
#[batch_impl(Pair3<*()^2>)]
trait GenSpl {}
// → impl<P0, P1> GenSpl for Pair3<P0, P1>(flattened into two args)§4.6 Legal positions
A splat is a parameter-position list: generic args / tuple / array elements / generic declarations / fn parameters / spec lists. A bare splat as a where-predicate subject is rejected (*(A,B): Trait has no defined semantics); a bare * that is neither a splat nor a raw pointer errors with a targeted message.
§5. Generics <>
§5.1 Declarations
<...> before the trait name declares impl generics — copied into the impl as-is:
#[batch_impl(<T> Vec<T>)]
trait T2 {}
// → impl<T> T2 for Vec<T> {}§5.2 A<> — copied as-is
An empty <> copies the trait’s own generics verbatim:
#[batch_impl(A<> Vec<u8>)]
trait A<T, const N: usize> {}
// → impl<T, const N: usize> A<T, N> for Vec<u8> {}§5.3 Args: multi-args, nesting, bindings
struct Map<K, V>(K, V);
struct A; struct B; struct C;
struct Wrap<X>(X);
#[batch_impl(Map<A, B>)] // multi-args
trait M1 {}
#[batch_impl(Map<Map<A, B>, C>)] // nested structure preserved (TyGeneric nesting)
trait M2 {}
#[batch_impl(Conv<u8, Item = u8> Wrap<u8>)] // associated-type binding (trait path)
trait Conv<T> { type Item; }§5.4 Operations inside <> (programmable in 0.7.0)
Generic-argument positions accept full DSL expressions — the structural landing of 0.7.0:
struct Wrap<X>(X);
struct Pair3<A, B>(A, B);
struct A2; struct B2;
#[batch_impl(Wrap<()^2>)] // generator: <P0,P1> Wrap<(P0,P1)>
trait GenTup {}
// → impl<P0,P1> GenTup for Wrap<(P0, P1)>(the tuple stays a single arg)
#[batch_impl(Pair3<*()^2>)] // generator splat: <P0,P1> Pair3<P0,P1>
trait GenSpl {}
// → impl<P0,P1> GenSpl for Pair3<P0, P1>(flattened into two args)
#[batch_impl(Wrap<@u*>)] // constant family: 6 impls (u8..usize)
trait ConstArg {}
#[batch_impl(Wrap<[A2, B2]>)] // array: 2 impls (Wrap<A2>/Wrap<B2>)
trait ListArg {}§5.5 Same-name inheritance and trait where inheritance
When the trait’s generic params share names with the spec’s args, bounds inherit automatically; renaming errors explicitly:
#[batch_impl(<T> Box<T> where{Box<T>: Clone})]
trait B2 {}
// → impl<T> B2 for Box<T> where Box<T>: Clone {}#[batch_impl(<T> Foo<U>)] // renamed (U ≠ T) → explicit error (not silent)
trait Foo<T> {}§6. The @ Constant System (macro-meta layer)
@ is the DSL’s reserved library-owned constant namespace — # is taken by the directive mechanism, so @ provides “name and reuse type-matrix entries”. It is pure lexical substitution (the macro-meta layer): the expanded result enters the pipeline and participates in no in-domain parsing.
§6.1 Built-in constants
Name families (a closed set — the language-defined type collections): @u*, @i*, @f*, @num, @scalar.
#[batch_impl(Box^@u*)] // Box applied to every member of @u*
trait BoxRc {}
// → impl BoxRc for Box<u8> {} / Box<u16> / ... / Box<usize>Range families: @u8..u128, @i8..i128, @f32..f64 (inclusive). usize/isize only enter name families, not range families.
§6.2 Lazy expansion and references
Constant values are stored as verbatim tokens; reference sites splice and expand recursively — a value can be a DSL expression (@uints=@uint) or a chained reference (@a=@b). Cycles/forward references are rejected at definition (preventing infinite recursion); a bare range endpoint reference (@a=@u8 without ..) errors at definition.
§6.3 Custom constant sections (batch_trait! only)
A leading @name=value; section defines reusable constants (values may chain
references and embed DSL expressions). #[batch_impl] / #[batch_impl_only]
do not support custom constants — the 0.7.2 feature was reverted in 0.8.0;
write attribute-macro matrices directly with ^/-/* instead:
batch_trait! {
@uints = @u*;
A: @uints;
B: <T> B<T> Vec<T>;
}Limit:
batch_trait!does not support#directives (#fill/#delegate/#blanket/open extension) — directives need the trait definition as the signature source of truth, andbatch_trait!is a function-like macro that never sees one. Use#[batch_impl]/#[batch_impl_only]when you need directives.
§6.4 The complete macro-meta layer: an addressing algebra + value classes
@’s positional references form an addressing algebra — not a flat list of notations:
| Notation | Derivation | Meaning |
|---|---|---|
@g_i | primitive — group g, slot i (stable across array distribution) | addresses a macro-generated generic (groups/slots number from 0; dangling refs are targeted errors) |
@N | @g_i flattened by document order within one impl | references a fresh generic (where{@0: Clone}) |
@all_fresh | all fresh generics | range sugar — “every one” |
@N..=M | a contiguous run | range sugar — @0..=1 = @0, @1 |
@N.. | an open run to the last fresh | range sugar — “from the second element on” (@1..); empty when N is past the end (an arity-1 impl contributes no such predicate, no error) |
Power-user tier:
@g_i/@all_fresh/@N..Mare advanced addressing notations — start from@u*/@all_methods/@0and reach for them only when a predicate must name a specific fresh. The whole DSL surface is frozen since 0.7.2 (see README); these notations will not change semantics again.
#[batch_impl(()^2 where{@0..=1: Clone})] // range sugar: @0..=1 = @0, @1
trait RangeSugar {}
// → impl<P0,P1> RangeSugar for (P0,P1) where P0: Clone, P1: Clone
#[batch_impl(()^3 where{@all_fresh: Copy})] // every fresh generic
trait AllFresh {}
// → impl<P0,P1,P2> AllFresh for (P0,P1,P2) where P0: Copy, P1: Copy, P2: Copy
#[batch_impl(()^3 where{@1..: Copy})] // open range: from index 1 on
trait OpenRange {}
// → impl<P0,P1,P2> OpenRange for (P0,P1,P2) where P1: Copy, P2: Copy
// (an arity-1 impl contributes no predicate — `@1..` is empty there)@N also resolves in value positions — the type after : may carry
@N inside angle groups, e.g. an associated-type binding referencing
another fresh’s associated type (the alga2 tuple Module scalar-equality
constraint):
#[batch_impl(
Module<(), ()> ()^1..=4 where{
@all_fresh: Module<(), (), Scalar: Copy>,
@1..: Module<(), (), Scalar = @0::Scalar>,
} impl{(A@..,)}
#Scalar{A0::Scalar}
#scale{( @(@A::scale(&self.@0, s),).. )}
)]
trait Module<Add, Mul> {
type Scalar;
fn scale(&self, s: Self::Scalar) -> Self;
}
// arity 2 → impl<P0,P1> Module<(), ()> for (P0,P1)
// where P0: Module<(), (), Scalar: Copy>, P1: Module<(), (), Scalar: Copy>,
// P1: Module<(), (), Scalar = P0::Scalar>The shared-scalar pattern: every component from the second one on declares
Scalar = @0::Scalar (the first component’s scalar), with @0 resolving to
the first fresh’s name. The @1.. open range is exactly the “from the
second component on” set — it shrinks with the tuple arity and disappears
for arity 1.
On the other axis (value classes):
| Notation | Class | Use |
|---|---|---|
@trait | identity — the current trait name/path (section-level in batch_trait) | package “generic declaration + trait name” across sections |
@all_methods etc. | selection — extract an item set from trait_def | #fill(@all_required_methods, -foo) precise selection |
@Cow etc. custom | package — a type plus its inherent constraints | reuse a “constrained wrapper” (see §7.4) |
@all family combined with - subtraction selects arbitrary item subsets (#fill(@all_required_methods, -foo)); @all_default* / @all_required* distinguish default implementations from required methods.
§7. The Directive System #
Directives copy item signatures from the trait definition (methods/consts/types all supported); the body is yours to fill — “declare data, not write repetitive code”.
§7.1 #name{body} — single-item assignment
#[batch_impl(usize #to_str{"usize"})]
trait ToString { fn to_str(&self) -> &str; }
// → impl ToString for usize { fn to_str(&self) -> &str { "usize" } }§7.2 #fill(methods){body} — many methods, one body
#[batch_impl((u32,) #fill([add, add2]){self.0 = self.0.wrapping_add(x as u32)})]
trait Ops { fn add(&mut self, x: u8); fn add2(&mut self, x: u8); }Filling a single method,
#fill([foo]){body}is equivalent to the single-item directive#foo{body}, which is more concise.
§7.3 #delegate(methods){target} — delegate calls
#[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() } }§7.4 #blanket(@all_methods){wrapper matrix} — blanket delegation
#[batch_impl(#blanket(@all_methods){Box})]
trait NumOps { fn inc(&mut self); }
impl NumOps for u32 { fn inc(&mut self) { *self += 1 } }
// → impl NumOps for Box<u32> { fn inc(&mut self) { (**self).inc() } }(delegates to the wrapped u32)By-value receivers:
fn consume(self)forwards as(*self).consume()— a by-valueselfIS the wrapper, one deref fewer (&selfmethods use(**self): through the reference, then the wrapper). Moving out cannot type-check for shared wrappers (&/Rc); the generated impls carry a#[doc]note (proc macros have no stable warning channel, E0658). Skip such methods with@all_ref_methods(the trait default stays) or hand-write#name{...}.
§@Cow — a constraint-carrying packing (the case study)
Cow<'_>’s deref target is T::Owned, not T — the naive (**self) delegation can’t pass type checking. @Cow packs Cow<'_> plus the inherent constraint predicates (@0: ToOwned + ?Sized, @0::Owned: @trait), making it blanket-usable. This is the demonstration that a constant carries reuse value only when it carries constraints:
#[batch_impl(#blanket(@all_methods){@Cow})]
trait CowLen { fn clen(&self) -> usize; }
impl CowLen for str { fn clen(&self) -> usize { self.len() } }
impl CowLen for String { fn clen(&self) -> usize { self.len() } }
// → impl CowLen for Cow<'_, str> ... / Cow<'_, String> ...(delegates via the packed predicates)§7.5 Open extension
An unknown #name(args){body} becomes a top-level macro call — DSL fills the spec body, you write the rest. The deliverable of this extension point is the protocol shape itself: batch-impl does not implement your codegen, it only guarantees the four-part input {spec}(args){body}trait_def reaches your same-named macro.
#[batch_impl(u16 {! batch_preprocess_test!{(add,inc){*self+3} trait AddIncU16 { fn add(&mut self, x: u16); fn inc(&mut self); }}})]
trait AddIncU16 { fn add(&mut self, x: u16); fn inc(&mut self); }The protocol has converged to one shape: the legacy in-impl form
T {m!{...}}(no!, the call lands in the impl body as associated items) is deprecated since 0.7.2 (kept for compatibility — no warning channel exists, so the deprecation lives in the docs). Write new extensions against the top-level{! m!{...}}four-segment protocol{spec}(args){body} traitonly.
§8. where Clauses
§8.1 where{...} suffix
#[batch_impl(Vec<u8> where{Vec<u8>: Clone})]
trait T {}§8.2 Bare where predicate {code block}
Rust-style constraint/body separation (the {...} code block after the predicate is required):
Equivalently,
where{predicates} {code block}(the §8.1 suffix + a chained body) can be written bare aswhere predicates {code block}, saving one{}layer.
#[batch_impl(u8 where u8: Clone { fn tag(&self) -> &'static str { "u8" } })]
trait T { fn tag(&self) -> &'static str; }§8.3 Predicate inheritance
Trait-level where clauses inherit into the impl; renaming/composite predicates referencing undeclared params error explicitly.
§8.4 impl{...} Self-part shape templates (0.8.0, Ext 2)
A third trailing attachment beside where{...} and {body} — the Self-part
shape template. The three kinds attach in any order. The block holds a
standard Rust type (DSL operators are rejected): it is matched against
the leaf target type position by position, and an ident that equals
the target’s ident at that position is a literal (kept as-is), while a
different one is a binding slot — rewritten in the target type, the where
predicates and the body. One body, adapted to every leaf:
#[batch_impl([Box, Rc]^u32 impl{W<T>} { fn mk(x: u32) -> W<T> { W::new(x) } })]
trait Make { fn mk(x: u32) -> Self; }
// → impl Make for Box<u32> { fn mk(x: u32) -> Box<u32> { Box::new(x) } }
// → impl Make for Rc<u32> { fn mk(x: u32) -> Rc<u32> { Rc::new(x) } }impl{T}+i32→T := i32(a bare ident template binds the whole leaf);impl{Rc<T>}+Rc<i32>→T := i32(Rcis equal → literal);impl{Rc<T>}+Box<i32>→Rc := Box, T := i32(different base → slot);- multiple
impl{...}merge into one mapping — identical re-bindings are legal, conflicting ones error (impl{X}binds the whole leaf,impl{X<u32>}binds the base —InconsistentBinding); - the attachment depth limit counts
impl{...}like the other kinds; @traitinside the template expands to the trait path before matching.
§Template matching: what binds and what does not
The template is matched against the leaf by structural recursion — every
syn::Type form is recognized and recursed into:
| Template form | Behaviour |
|---|---|
T (bare ident) | binds the whole leaf subtree |
Rc<T> / std::rc::Rc<T> (path, multi-segment ok) | base/segment idents: equal → literal, different → slot; generic args recurse |
&A / &mut A / *const A / *mut A | the reference/pointer lifetime & mutability are structural; the element binds |
[A] (slice), (A, B, C) (tuple) | elements bind position by position |
[A; 3] (fixed array, literal length) | the length compares verbatim; the element binds |
[A; N] (fixed array, const-param length) | the length binds to the leaf’s length (N := 3; the body may use N) |
Cow<'_, A> (lifetime arg) | '_' is a wildcard matching any lifetime; 'a vs 'b compares verbatim; the type arg binds |
Not bindable (kept as verbatim comparison — a targeted diagnostic instead of a silent mis-bind):
- slots inside fn-pointer / trait-object templates (
fn(A) -> B,dyn A + Send): these forms are compared verbatim — only an identical template matches itself; - cross-class argument binding (
Cow<'_, A>vs a 1-argBox<u8>leaf;Foo<A>vsFoo<3>): a lifetime/const argument cannot bind to a type argument, and mismatched arities cannot align. Write one prototype template per shape family instead (below).
§The prototype-impl pattern
Write one correct implementation for a representative leaf, and the “equal → keep, different → bind” rule adapts it to every leaf of the matrix:
#[batch_impl([Box, Rc]^@num impl{Box<u8>} #max{Box::new(u8::MAX)})]
trait TMax { fn max() -> Self; }
// → impl TMax for Box<u8> { fn max() -> Box<u8> { Box::new(u8::MAX) } }
// → impl TMax for Box<u16> { fn max() -> Box<u16> { Box::new(u16::MAX) } }
// → impl TMax for Rc<f64> { fn max() -> Rc<f64> { Rc::new(f64::MAX) } }Each shape family needs its own prototype (a Cow<'_, u8> template covers
the Cow family — the lifetime '_' wildcard matches any leaf lifetime).
Combine families in one attribute, either as separate specs or as pairs with
a list-wide distribution:
#[batch_impl(
[[Box, Rc] impl{Box<u8>},
Cow<'_> impl{Cow<'_, u8>}]^@num #tag{1}
)]
trait Tag { fn tag() -> usize; }
// Box<u8>..Rc<f64> covered by the Box<u8> prototype; Cow<'_, u8>..Cow<'_, f64>
// covered by the Cow prototype — one attribute, two shape families§Variadic segments and repeat blocks
An impl{...} template can declare a variadic segment with ident@..:
it covers every remaining tuple position from its own position onward (a
segment written after fixed elements starts at their count). The segment’s
names are aligned with the leaf position — (u8, A@..,) on
(u8, u16, u32) yields A1, A2 (there is no A0; the index cursor
starts at @1), while (A@..,) on (u8, u16, u32) yields A0, A1,
A2. Same-level segments split the leaf evenly ((A@.., B@..,) on an
arity-4 leaf → A len 2, B len 2); an uneven split errors. Segments recurse
into nested tuples (((A@..,),(B@..,))), and duplicate segment prefixes in
one template error.
The body repeats with @(...).. — a repeat block emitted once per element
of the segment(s) it references:
#[batch_impl((u8, u16, u32) impl{(A@..,)} { fn tail(&self) -> (u8, u16, u32) { (@(@A::from(self.@0),)..) } })]
trait ShapeTail { fn tail(&self) -> (u8, u16, u32); }
// body → (A0::from(self.0), A1::from(self.1), A2::from(self.2))
// → (u8::from(self.0), u16::from(self.1), u32::from(self.2))@identinside a block is a name reference — the i-th element’s slot name (A0,A1, …), which the slot mapping then rewrites to the bound leaf element;@Nis an index cursor — the numeric literalN + i; write the path prefix yourself (self.@1for a segment starting at leaf index 1);- the block repeats
Ltimes, and the length comes from one of three sources: the segments referenced inside (@ident, all equal-length), a declared driver (@A(self.@0,)..— the segment named right after@, useful for cursor-only bodies), or — for a cursor-only block with no declared driver — the template’s unique segment (an arity-shape with several segments rejects the ambiguous cursor-only form); - the block body’s trailing
,is the separator, emitted after every round — write no comma between side-by-side blocks (each block already terminates its own elements); - nested blocks run independent rounds (Cartesian semantics);
- outside a block,
@in a body is an error.
A cursor-only block generates element references without naming the types — the tuple-to-tuple re-shaping case:
#[batch_impl((u8, u16, u32) impl{(A@..,)} { fn elems(&self) -> (u8, u16, u32) { (@(self.@0,)..) } })]
trait ShapeElems { fn elems(&self) -> (u8, u16, u32); }
// body → (self.0, self.1, self.2)
// (the single-segment template supplies the length; `@A(self.@0,)..` is the
// explicit spelling, also valid for multi-segment templates)The alga2-style end-to-end — one spec covers every tuple arity, with
@all_fresh constraining every fresh generic:
trait Magma { fn combine(&self, rhs: &Self) -> Self; }
impl Magma for u8 { fn combine(&self, rhs: &Self) -> Self { *self + *rhs } }
#[batch_impl(
()^1..=2 where{@all_fresh: Magma} impl{(A@..,)}
#combine{( @(@A::combine(&self.@0, &rhs.@0),).. )}
)]
trait TupleMagma { fn combine(&self, rhs: &Self) -> Self; }
// → impl<A0> TupleMagma for (A0,) where A0: Magma { ... }
// → impl<A0, A1> TupleMagma for (A0, A1) where A0: Magma, A1: Magma { ... }§8.5 The ItemImpl entry (0.8.0, Ext 1)
#[batch_impl] also accepts an impl block: the DSL describes a
shape template × matrix source, every matrix leaf emits one impl, and
the slot mapping (the same “equal → keep, different → bind” rule as
impl{...}) rewrites the for-Type / where predicates / body. The original
impl (whose for-Type holds the placeholder slots) is withheld:
#[batch_impl(A<B> : [Box, Rc]^[usize, isize])]
impl Make for A<B> { fn make() -> A<B> { A::new(B::default()) } }
// → impl Make for Box<usize> { fn make() -> Box<usize> { Box::new(usize::default()) } }
// → ... × 4- Attr grammar: shape form
A<B> : [Box,Rc]^[usize,isize](template:matrix) or the direct form<T> Box<T>(generic declaration + for-type, N = 1);;separates multiple specs (W:u8; W:u16), the single-spec case is the common one; @trait(→ the impl’s trait path) is allowed in generic-decl bounds and where predicates; custom@constants,@N/@g_irefs and#directives are rejected on this entry;- the impl’s own generics / where clause /
unsafeare preserved; the bare where region also ends at a depth-0;or the end of the stream.
§9. Tuple Generation and Matrices
§9.1 Tuple generators
(T,)^N generates tuples of length 1..=N; ()^N generates N fresh params:
#[batch_impl((u8,)^3)]
trait T {}
// → impl T for (u8,) {} / (u8, u8) / (u8, u8, u8)§9.2 Cartesian products
[A, B]^[C, D] full combinations; *(A,B)^2 splat pow produces a Cartesian combo list:
#[batch_impl([Box, Rc]^[u8, u16])]
trait Matrix {}
// → impl Matrix for Box<u8> {} / Box<u16> / Rc<u8> / Rc<u16>(4 entries)Matrices can be wrapped into containers or const-generic fixed arrays (([u8, u16],)^2 etc.).
§10. The Modifier Gallery
| Modifier | Meaning | Example |
|---|---|---|
& / &mut | reference | &^Box^T = &Box<T> |
*const / *mut | raw pointer | *const^T = *const T |
unsafe | unsafe fn | unsafe^fn^(A,B)-C |
#[...] attributes | attribute on the impl | #[cfg(...)] gating |
! | never type | !^T |
§11. Three Entry Points
#[batch_impl]— annotates the trait definition, re-emits it and generates impls (one trait per macro).#[batch_impl_only]— generates impls only, the trait comes from outside (for traits you don’t own, or already declared):
#[batch_impl_only(Conv<bool> Wrapper<bool> #conv{false})]
trait Conv<T> { fn conv() -> T; }
// → impl Conv<bool> for Wrapper<bool> { fn conv() -> bool { false } }(trait not re-emitted)batch_trait!— a function-like macro for an already-declared trait, multi-section support, custom@name=value;constant sections, no directives.- ItemImpl entry (0.8.0, Ext 1) —
#[batch_impl]also accepts animplblock: batch-instantiate a hand-written impl from a shape template × matrix source (see §8.5).
§12. Error Hints
batch-impl’s errors are compile-time diagnostics pointing at the user-visible token closest to the root (macro-generated artifacts fall back to the macro-call line):
- Missing operand:
A^/^A/,A—compile_error!with a clear message - Unknown
@constant: lists the built-in names (@u*/@i*/@f*/@scalar/@num+ range families) - Constant cycle/forward reference: rejected at definition (prevents infinite recursion)
@N/@g_iout of range or dangling:@5beyond the impl’s generated generic count /@2_0group missing — targeted errors in user language, no reserved_Param_*_BatchGen_names leaked (and no raw rustc E0412 either)- Splat as a where-predicate subject: explicitly rejected (
A, B: Traithas no defined semantics) - Generic rename breaks inheritance: renaming a trait generic param = explicit error, never silent
- Bare
*(neither splat nor pointer): targeted error instead of rustc raw-pointer confusion - Empty range (
@u16..u8): “no impls generated for empty range” =/:in concrete-type args: bindings/bounds are trait-path/declaration-only — targeted error (Assoc<Item = u32>with a struct reports “binding args are only valid on a trait path”)- Adjacent types without an operator:
A B/Vec<T>U/[A B]— “missing^/-/,” instead of rendering invalid Rust - Stray
;/=/@/#in a type position: targeted error (the=of..=excluded — no cascading second diagnostic) - Trailing tokens after an
fnparameter list:fn(A) B/fn(A)->— unexpected-token error (a return type is-> Bor-B) - Blanket method returns
Self:#blanketcannot delegate a method returningSelf/Self::Assoc(forwarding yields the inner type, not the wrapper’sSelf) — error with a#name{...}suggestion - Empty binding/bound value:
Conv<Item =>/Conv<T:> X— “missing a value” / “missing a bound” - Non-integer type literal:
1.5/"hi"/'a'— only an integer (usize) is a type - Non-integer range endpoint:
1..x/A..B— “needs integer endpoints” - Malformed array length:
[u8; 3; 4]/[u8;]— “missing or malformed” +/?/.at a type start:+A/?Sized/.foo— “not valid at the start of a type”- Unknown-directive typo suggestion:
#delgate/#blanlet— “did you mean#delegate?” (open-extension names farther than 2 stay silent)
Macros§
- batch_
impl_ blanket - Documentation placeholder for the
#blanketdirective. - batch_
impl_ consts - Documentation placeholder for the
@macro-meta constant system. - batch_
impl_ delegate - Documentation placeholder for the
#delegatedirective. - batch_
impl_ fill - Documentation placeholder for the
#filldirective. - batch_
impl_ name - Documentation placeholder for the
#name{body}fill-by-name directive. - batch_
impl_ open - Documentation placeholder for the open-extension protocol.
- batch_
preprocess_ test - Reference open-extension macro — the reference implementation of the open-extension protocol (also the test consumer) (function-like):
name!{ {spec}(method name list){body} trait T {...} }. - batch_
preview - The DSL-aware expansion preview: wrap the exact attribute-macro form you would feed
#[batch_impl]and the preview reports every generated impl through acompile_error!message (the only stable terminal channel a proc macro has). The message is the expansion verbatim — the trait plus the impls, exactly what#[batch_impl]emits — followed by preview-only guidance. - 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.