Skip to main content

Crate batch_impl

Crate batch_impl 

Source
Expand description

§batch-impl

v0.9.2 (2026-08-21) — @N.. / @N..M fresh ranges work anywhere a single @N can: where predicates (@1..::Output: Clone, the tail copied per fresh), <> generic args, the impl-generic declaration (<@0..>), tuple targets — plus grouped ranges @L_N.. / @L_N..M within one generator group (stable across array dispatch). Variadic segments no longer need a trailing comma (impl{(A@..)}). Pre-0.9 changelog entries use the ^ operator of their time again (the 0.9.0 release pass had rewritten them to .).

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) {}

§Built with batch-impl

alga2 is a real user — a modern abstract-algebra hierarchy for Rust (the successor to alga), with ~900 impls generated from ~80 batch-impl DSL blocks across 15+ types (numbers, tuples 1–16, arrays, Option, Complex, Quaternion, ModN, smart pointers, collections). alga2 0.1.0 is released on crates.io; the batch-impl DSL has been its impl generator throughout development.

§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, space/. application, ().N tuple 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 }? )]
SymbolMeaningIntuition
space / .apply: apply the left container/modifier to the right typethe same operation, only associativity differs
[A, B]listhorizontal expansion (Cartesian product)
(A, B)tuplepermutations (ordered pairs)
*[...] / *(...)splat: flatten into the enclosing list[a, *[b,c]] = [a,b,c]; left *[...] distributes / *(...) appends
#namedirective: auto-copy the item signature from the trait definitionthe body doesn’t hand-write signatures

The space (adjacency) is the natural way to apply: the left side is the modifier/container/trait, the right side the target type, and chaining accumulates arguments left-associatively — HashMap u32 String = HashMap<u32, String>, fn(A, B) C = fn(A, B) -> C, Tr u8 = impl Tr for u8 (a bare trait name applies as the impl trait; the trait name is identified by the annotated trait). Write Tr<u8> for the type Tr<u8>.

. is the same operation with right-associative grouping, for nesting only: Box.Box u8 = Box<Box<u8>>, HashMap<K> String = HashMap<K, String> (space works here too).

Pick by the grouping shape you want: use the space to list arguments side by side, use . to nest.

[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

FeatureIn one sentenceTutorial chapter
Side-by-side lists [A, B]Implement for multiple types at once, body reused§3
Splat * prefixFlatten containers/generators into the enclosing list — in-list splice, . right-operand flat append, generic multi-arg; left operand *[...] distribute / *(...) append§4
space / . operatorsLeft/right associativity of the same operation: accumulation vs. nesting§2
Generic automationA<> copied as-is, same-name inheritance, trait where-clause inheritance§5
Associated type bindingsIter<Item=T>type Item = T;§5.3
Directive system #name/#fill/#delegateAuto-copy signatures, batch-fill bodies, delegate calls§7
Blanket delegation #blanketGenerate 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 extensionUnknown #name(args){body} becomes a top-level macro call: your same-named macro receives {spec}(args){body}trait and emits its own impl§7
@ constantsBuilt-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 blocksident@.. 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 / attributesFull support for type-level modifiers (unsafe fn is the fn type; unsafe.fn marks the impl unsafe)§10

Shorthand: a single method #fill([foo]){body} equals #foo{body}; predicates + code block where{predicates} {code block} can be written bare as where predicates {code block} (see §7.2 / §8.2).

§Syntax-freeze commitment (0.7.2)

The semantics of every existing token are final./space, []/()/<>, 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)
  • impl entry / shape template (0.8.0): the ItemImpl entry#[batch_impl] also accepts an impl block and batch-instantiates it from a shape-template × matrix-source (tutorial §8.5); the impl{...} 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 like Cow (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(...)] impl input and read the real expansion, plus space/. 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: a Num/UNum/INum/FNum hierarchy + 36 From<bool> impls for Frac<T, U>)
  • Developers: internal architecture in docs/architecture.md, development changelog in docs/dev-changelog.md

§License

MIT OR Apache-2.0

§batch-impl Tutorial

v0.9.2 (2026-08-21) — @N.. / @N..M range references now work anywhere a single @N can: where predicates (@1..::Output: Clone — the tail after the range is copied per fresh), <> generic args (Wrapper<@0..> — one position re-opens into several), the impl-generic declaration (<@0..>), and tuple targets; grouped ranges @L_N.. / @L_N..M slice within one generator group; variadic segments no longer need a trailing comma (impl{(A@..)}); see §6.4 / §8.4;

v0.9.1 (2026-08-21) — a stability release: +A at the start of a spec gets a targeted “not valid at the start of a type” diagnostic instead of silently generating 0 impls; fn(A) -> ! { body } attaches the body to the impl (the ! block no longer swallows a trailing {...}); self is the identity prefix — a bare-type placeholder in matrices ([Box, self] u8 = Box<u8> + the bare u8, see §10);

v0.9.0 (2026-08-21) — the apply operators are reworded: . is the right-associative apply operator and space application replaces - as the left-associative combination (see §2); the DSL is a bag of blocks (declarations / directive blocks / code blocks / types in any order, folded by apply); same-name generic declarations merge into a where clause; _ is a never-replaced wildcard in impl{...} templates; X<> syncs to the spec trait application via a switch template (impl{@trait<>} / impl{Tr<>});

v0.8.3 (2026-08-19) — the builtin-directive typo guard is removed: single-item #name{...} may legitimately collide with fill/delegate/blanket (see §7);

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):

PartNotationRole
apply system. / space / [] / ()Type matrix: apply the left container/modifier to the right type, lists expand into multiple impls
directive system#name / #fill / #delegate / #blanketCopy 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 }?
PartExampleWhen 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 typeusize, Vec<T>, &strrequired
{ body }{ fn m(&self) -> usize { 0 } }when a custom body is needed

Multiple specs are separated by ,: #[batch_impl(usize, isize)].

§2. Type Matrix: the space (and .)

The space is the natural way to apply: write the container/modifier and the types it takes side by side — chaining accumulates arguments left-associatively.

What the space actually is: a space is not a token — it is the gap between tokens (proc-macro2 strips whitespace, so the DSL sees only adjacency). A space application therefore means “these tokens are adjacent, apply them” (Box u8 = Box<u8>), which is exactly how Rust itself reads type syntax (Box<u8> is Box adjacent to <u8>). No explicit operator symbol is needed — the absence of a separator is the operator.

WritingExpansion
Box u32Box<u32>
HashMap u32 StringHashMap<u32, String> (left-associative accumulation)
Box u8 u16Box<u8, u16>
fn.(A,B) Cfn(A,B)->C (or write fn(A,B) -> C)
& u8&u8 (chained modifiers)
Tr u8impl Tr for u8 (a bare trait name)
[Box, Vec] u32Box<u32>, Vec<u32> (lists expand)
HashMap<u8> StringHashMap<u8, String> (prefilled generics appended)
Box [u8, u16]Box<u8>, Box<u16> (list distributes)
[Box, Vec] [u8, u16]Cartesian product, 4 entries

. is the same operation with right-associative grouping — reach for it only when you want nesting instead of accumulation. Space accumulation puts arguments side by side (Box Box u8 = Box<Box, u8> — a typo for most containers); . nesting composes them (Box.Box u8 = Box<Box<u8>>):

WritingExpansion
Box.Box u8Box<Box<u8>> (right-associative nesting)
&.Box u8&Box<u8> (modifier over the nested type)
[Box, Vec].TBox<T>, Vec<T>
Box.[T1, T2]Box<T1>, Box<T2>
[HashMap<K>, Vec<K>].VHashMap<K, V>, Vec<K, V>

When to use which: one container/modifier + one type — write them side by side (Box u8, & u8, HashMap<u8> String). When the type itself needs to be a composed type (Box<Box<u8>>, &Box<u8>), join the composition with . — the space would treat each part as a separate argument.

The bare trait name applies as the impl trait: Tr u8 = impl Tr for u8, Tr<A> u8 = impl Tr<A> for u8. Write Tr<u8> for the type Tr<u8>.

Precedence from low to high: ; < , < space < .; () grouping sits above all operators.

Note: Box.Vec u32 is wrong (it parses as Box<Vec, u32>); write Box.Vec.u32 instead. When you miswrite it, rustc’s E0107 error prints the rendered Box<Vec, u32> verbatim — the mistake is self-evident.

Operand strictness: both sides of ./space/, must have operands — A., .A, ,A, A,,B all report compile_error!; only trailing commas (A, / [A, B,]) are allowed, and ()/[] brackets are real tokens, not empty operands. ; stays lenient as a batch_trait! section boundary.

#[batch_impl(Box.Vec.u32, HashMap<u8> String)]
trait T {}
// → impl T for Box<Vec<u32>> {}   ← `.` nesting: Box applied to 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.

Pythonbatch-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 applies 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)

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 ./space/* 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, and batch_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:

NotationDerivationMeaning
@g_iprimitive — 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 implreferences a fresh generic (where{@0: Clone})
@all_freshall fresh genericsrange sugar — “every one” (≡ @0..)
@N..=Ma contiguous runrange sugar — @0..=1 = @0, @1
@N..an open run to the last freshrange 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..M are advanced addressing notations — start from @u* / @all_methods / @0 and 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{@0..: Copy})]       // = @all_fresh (from 0 to the last fresh)
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)

@all_fresh and @0.. are equivalent; the @N.. family is the preferred spelling (@0.. covers the whole run, @1.. its tail).

Ranges work anywhere a single @N can (0.9.2): beyond the where predicates above, the range’s tail may be an associated-type path, copied per fresh — and a range in a target position re-opens against the fresh list the spec’s generators produced:

struct Wrap3<A, B, C>(A, B, C);
#[batch_impl(Wrap3<*().3> where{@0..: Clone} { fn m(&self) {} })]
trait RangeAngle { fn m(&self); }
// → impl<P0,P1,P2> RangeAngle for Wrap3<P0,P1,P2> where P0: Clone, P1: Clone, P2: Clone

trait HasOut { type Out; }
#[batch_impl(Wrap3<*().3> where{@0..: HasOut, @0..::Out: Clone} { fn m(&self) {} })]
trait RangeAssoc { fn m(&self); }
// → where P0: HasOut, P0::Out: Clone, P1: HasOut, P1::Out: Clone, P2: HasOut, P2::Out: Clone

The fresh list a range indexes comes from the spec’s generators (*().N / ().N); a range in a spec with no fresh generics reports “out of range”.

The impl-generic declaration position works too: <@0..> declares every fresh the range covers as an impl param — so a spec can put the generator in the trait args (GenConv<*().2>) and reference the same fresh batch in the declaration and the predicates:

struct DeclTarget;
#[batch_impl(<@0..> GenConv<*().2> DeclTarget where @0..: Clone { fn m(&self) {} })]
trait GenConv<T, U> { fn m(&self); }
// → impl<P0,P1> GenConv<P0,P1> for DeclTarget where P0: Clone, P1: Clone

(An empty <@0..> — no fresh generators in the spec — contributes no parameters, like an empty @1.. predicate.)

Grouped ranges @L_N.. (0.9.2) slice within one generator group — the in-group counterpart of @g_i, stable across array dispatch. With several generators in one spec (<*().2> → group 0, <*().3> → group 1), @1_0.. constrains only group 1’s fresh:

struct MultiTarget;
#[batch_impl(
    <@0..> <@1..> PairGen<*().2, *().3> MultiTarget where{@1_0..: Clone}
    { fn m(&self) {} }
)]
trait PairGen<A, B, C, D, E> { fn m(&self); }
// → impl<P0,P1,P2,P3,P4> PairGen<P0,P1,P2,P3,P4> for MultiTarget
//     where P2: Clone, P3: Clone, P4: Clone   ← group 1 only (P0,P1 unconstrained)

@L_N.. (open to the group’s end), @L_N..M and @L_N..=M (closed) all work; an unknown group errors like @g_i.

@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{
        @0..: 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):

NotationClassUse
@traitidentity — 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. custompackage — a type plus its inherent constraintsreuse 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.

X<> (empty angle brackets on the same-named trait) in a where predicate or an impl{...} template syncs to the spec trait application — write Semiring<> instead of repeating Semiring<Additive, Multiplicative>:

#[batch_impl(
    Semiring<Additive, Multiplicative> ().1..=2 where{@0..: Semiring<>},
)]
trait Semiring<Oa, Om> {}
// → impl<P0> Semiring<Additive, Multiplicative> for (P0,)
//     where P0: Semiring<Additive, Multiplicative>
// → ... arity 2 (P1 gets the same predicate)

@trait<> is equivalent (@trait expands to the trait path first). A X<> for any trait other than the spec’s errors; a trait with no generic arguments syncs to the bare name (Tr<>Tr). The body syncs via a switch template impl{Tr<>} — a template holding only the empty-bracket trait, which does not match Self; it only declares that the body’s Tr<> references sync too (the body is arbitrary Rust, so a Vec<> there is not a trait reference).

§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

Wraps any type (smart pointers included); wrappers are comma-separated, and a :N suffix marks the deref depth:

#[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)

#[batch_impl(#blanket(@all_methods){&, Box})]
trait Len { fn len(&self) -> usize; }
// → impl<T: Len> Len for &T     { fn len(&self) -> usize { (*self).len() } }
// → impl<T: Len> Len for Box<T> { fn len(&self) -> usize { (**self).len() } }

:N deref depth — how many layers the delegation dereferences to reach the inner T. Default 1 for single wrappers (&, Box, Rc): the body derefs N+1 times (&/Box**self). A :N of 2 means the wrapper itself is nested two deep — Box.Arc:2 = Box<Arc<T>>, delegation ***self. Write :2 only for nested wrappers; single wrappers need nothing.

By-value receivers: fn consume(self) forwards as (*self).consume() — a by-value self IS the wrapper, one deref fewer (&self methods 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} trait only.

§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 as where 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 The impl{...} shape templates (0.8.0)

The idea in one sentence: pattern matching + text substitution. You write one impl{...} block holding a prototype type, and the macro matches it against each leaf target type — positions that match (same ident) stay as-is, positions that differ become named slots, and those slot names are substituted into the body (and where predicates). 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) } }

How the match works, in plain terms:

  • The template is a pattern over the leaf type, compared position by position: impl{W<T>} against the leaf Box<u32>WBox so W is a slot (W := Box), Tu32 so T is a slot (T := u32). Against Rc<u32>W := Rc, T := u32. The template itself is not an impl target — it only declares the slots.
  • The slots are substituted — every occurrence of W/T in the body (and in where predicates) is replaced with the bound leaf part. That is the whole mechanism: pattern-match the leaf, collect slots, substitute.
  • Bare impl{T} binds the whole leaf (impl{T} + i32T := i32); impl{Rc<T>} + Rc<i32> → only T := i32 (Rc matched, kept).
  • Multiple impl{...} in one attribute merge into one mapping — identical re-bindings are legal, conflicting ones error.
  • @trait inside the template expands to the trait path before matching.

The template holds a standard Rust type — DSL operators are rejected inside it; _ is a wildcard that matches anything and stays _ (see the template-matching table below).

§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 formBehaviour
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 Athe 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-arg Box<u8> leaf; Foo<A> vs Foo<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). A trailing segment needs no commaimpl{(A@..)} and impl{(A@..,)} are equivalent (0.9.2; the trailing comma is supplied automatically so the template still parses as a tuple). 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))
  • @ident inside 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;
  • @N is an index cursor — the numeric literal N + i; write the path prefix yourself (self.@1 for a segment starting at leaf index 1);
  • the block repeats L times, 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 @0.. (≡ @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{@0..: 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 impl entry (0.8.0, ItemImpl)

Same idea, bigger template: the whole impl block becomes the pattern. Instead of a separate impl{...} attachment, you hand #[batch_impl] an ordinary impl block whose for-Type holds the placeholder slots (impl Make for A<B>), plus a template : matrix source. Every matrix leaf is matched against the for-Type (A<B>), the slots (A := Box, B := usize) are substituted into the whole block — for-Type, where predicates and body — and one impl per leaf is emitted. The original impl (holding the 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

In one sentence: write one impl with placeholders, get one impl per matrix cell — the same match-and-substitute as §8.4, applied to the whole block instead of just a body.

  • 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_i refs and # directives are rejected on this entry;
  • the impl’s own generics / where clause / unsafe are 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.).

ModifierMeaningExample
& / &mutreference& Box<T> — nest a composed type with .: &.Box u8 = &Box<u8>
*const / *mutraw pointer*const T = *const T
unsafeunsafe fn / unsafe impl markerunsafe.fn.(A, B) C = unsafe impl ... for fn(A, B) -> C
#[...] attributesattribute on the impl#[cfg(...)] gating
!never as a fn return typefn(A) -> ! (a ! block has no apply meaning; a trailing {...} belongs to the impl)
selfidentity prefixself T = T — in a matrix, [Box, self] u8 = Box<u8> + bare u8

self is the identity prefix: self T = T. In a matrix it acts as a bare-type placeholder[Box, self] u8 generates both Box<u8> and the bare u8:

#[batch_impl([Box, self] u8 { fn tag(&self) -> &'static str { "x" } })]
trait WrapOrBare { fn tag(&self) -> &'static str; }
// → impl WrapOrBare for Box<u8> {} / impl WrapOrBare for u8 {}

! (never) as a fn return type: fn(A) -> ! is legal — the ! block has no apply meaning, and a trailing {...} belongs to the impl:

#[batch_impl(fn(u8) -> ! { fn call(&self, _: u8) -> ! { unreachable!() } })]
trait NeverRet { fn call(&self, x: u8) -> !; }
// → impl NeverRet for fn(u8) -> ! { fn call(&self, _: u8) -> ! { unreachable!() } }

unsafe has two rolesunsafe fn(A) -> B is an unsafe fn type: the impl itself stays safe (impl Tr for unsafe fn(A) -> B). To mark the impl unsafe, apply unsafe with .: unsafe.fn(A) -> B = unsafe impl Tr for fn(A) -> B. If you find yourself writing unsafe fn(...) and expecting an unsafe impl, that is the wrong form.

§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). A # path::To::Trait: prefix declares the external trait’s real path (requires at least one ::; @trait and path references then use it):
#[batch_impl_only(# path::to::Conv: 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.
  • The impl entry (0.8.0, ItemImpl)#[batch_impl] also accepts an impl block: 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 / ,Acompile_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_i out of range or dangling: @5 beyond the impl’s generated generic count / @2_0 group 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: Trait has 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”)
  • Stray ;/=/@/#/- in a type position: targeted error (the = of ..= excluded — no cascading second diagnostic; a lone - is the retired operator — the exclusion lives only in directive lists)
  • Trailing tokens after an fn parameter list: fn(A) B / fn(A)-> — unexpected-token error (a return type is -> B or fn(A) B)
  • Blanket method returns Self: #blanket cannot delegate a method returning Self/Self::Assoc (forwarding yields the inner type, not the wrapper’s Self) — 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 — “not valid at the start of a type” (+ belongs in a bound, e.g. T: Clone + Send); a leading . reports a missing operand; ?-prefixed types (?Sized) pass through and rustc reports them
  • 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 #blanket directive.
batch_impl_consts
Documentation placeholder for the @ macro-meta constant system.
batch_impl_delegate
Documentation placeholder for the #delegate directive.
batch_impl_fill
Documentation placeholder for the #fill directive.
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 a compile_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 impl blocks for a declared trait in batch.

Attribute Macros§

batch_impl
Attribute macro that generates impl blocks for a trait in batch.
batch_impl_only
Same as #[batch_impl], but discards the annotated trait definition and only emits impl blocks.