Skip to main content

burn_backend_extension/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3
4use proc_macro2::TokenStream as TokenStream2;
5use syn::parse::{Parse, ParseStream};
6use syn::punctuated::Punctuated;
7use syn::{
8    Data, DeriveInput, Fields, FnArg, GenericArgument, Ident, ItemTrait, Meta, Pat, PathArguments,
9    ReturnType, Token, TraitItem, Type, TypeParamBound, parse_macro_input,
10};
11
12/// # `backend_extension`
13///
14/// Attribute macro that generates dispatch glue for Burn backend extension traits.
15///
16/// ## Usage
17///
18/// ```rust,ignore
19/// use burn_backend_extension::backend_extension;
20///
21/// #[backend_extension(Wgpu, Cuda, Cpu, Autodiff)]
22/// pub trait MyExtension: Backend {
23///     fn fused_matmul_add_relu(lhs: FloatTensor<Self>, rhs: FloatTensor<Self>, bias: FloatTensor<Self>) -> FloatTensor<Self>;
24///     fn custom_threshold(x: FloatTensor<Self>, threshold: f32) -> FloatTensor<Self>;
25/// }
26/// ```
27///
28/// ### What gets generated
29///
30/// - An `impl Trait for Dispatch` is generated automatically.
31/// - Each method dispatches to the corresponding implementation for the listed backends.
32/// - If `Autodiff` is specified, autodiff variants are also handled automatically.
33/// - All other backends are left as `unimplemented!()`.
34///
35/// Supported tensor argument/return types: `FloatTensor<Self>`, `IntTensor<Self>`,
36/// `BoolTensor<Self>`, and `QuantizedTensor<Self>` (a QFloat tensor passed to the
37/// backend still quantized — the op reads the packed values/scales directly instead
38/// of going through a dequantize).
39///
40/// ### Struct / enum inputs
41///
42/// A custom struct or enum of tensor primitives (deriving [`ExtensionType`](macro@ExtensionType))
43/// can be passed as an **input** by marking the argument with `#[extension_type]`:
44///
45/// ```rust,ignore
46/// #[derive(ExtensionType)]
47/// pub struct Inputs<B: Backend> { pub lhs: FloatTensor<B>, pub rhs: FloatTensor<B> }
48///
49/// #[derive(ExtensionType)]
50/// pub enum Operand<B: Backend> { Dense(FloatTensor<B>), Empty }
51///
52/// #[backend_extension(Wgpu)]
53/// pub trait MyExtension: Backend {
54///     fn fused(#[extension_type] inputs: Inputs<Self>, alpha: f32) -> FloatTensor<Self>;
55/// }
56/// ```
57///
58/// The macro unwraps the dispatch form (`Inputs<Dispatch>`) back into the concrete `Inputs<Wgpu>`
59/// before calling the backend impl. Such inputs may be freely mixed with bare tensor inputs and with
60/// each other (several are allowed), and the op may be combined with `Autodiff` (the op's own
61/// `impl ... for Autodiff<B>` hand-writes the backward pass, as for bare-tensor autodiff ops).
62///
63/// The backend is selected by walking the inputs at runtime for a representative tensor (preferring a
64/// float). Because an enum's tensors depend on its active variant, a tensor-less variant contributes
65/// no representative and the walk falls through to the next input; if no input carries any tensor the
66/// backend is unresolvable and the op panics.
67#[proc_macro_attribute]
68pub fn backend_extension(attr: TokenStream, item: TokenStream) -> TokenStream {
69    // Parse the backend list
70    let backends = parse_macro_input!(attr as Backends);
71    // Parse the trait definition
72    let trait_def = parse_macro_input!(item as ItemTrait);
73
74    // Lower to extension representation, then expand codegen
75    let expanded = lower_extension(backends, &trait_def)
76        .map(|ir| expand_extension(ir, trait_def))
77        .unwrap_or_else(|err| err.to_compile_error());
78
79    TokenStream::from(expanded)
80}
81
82#[derive(Debug, Clone)]
83struct Backend {
84    pub kind: BackendKind,
85    pub cfg: Option<Meta>,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89enum BackendKind {
90    Cpu,
91    Cuda,
92    Rocm,
93    Metal,
94    Vulkan,
95    Wgpu,
96    WebGpu,
97    Flex,
98    NdArray,
99    LibTorch,
100    Remote,
101}
102
103impl BackendKind {
104    fn try_from(ident: &Ident) -> syn::Result<Self> {
105        match ident.to_string().as_str() {
106            "Cpu" => Ok(BackendKind::Cpu),
107            "Cuda" => Ok(BackendKind::Cuda),
108            "Wgpu" => Ok(BackendKind::Wgpu),
109            "WebGpu" => Ok(BackendKind::WebGpu),
110            "Metal" => Ok(BackendKind::Metal),
111            "Rocm" => Ok(BackendKind::Rocm),
112            "Vulkan" => Ok(BackendKind::Vulkan),
113            "Flex" => Ok(BackendKind::Flex),
114            "NdArray" => Ok(BackendKind::NdArray),
115            "LibTorch" => Ok(BackendKind::LibTorch),
116            "Remote" => Ok(BackendKind::Remote),
117            other => Err(syn::Error::new_spanned(
118                ident,
119                format!("Unsupported backend `{}`", other),
120            )),
121        }
122    }
123}
124
125struct Backends {
126    concrete: Vec<Backend>,
127    autodiff: (bool, Option<Meta>),
128}
129
130// Helper to parse backend idents w/ optional cfg
131struct BackendArg {
132    id: Ident,
133    cfg: Option<Meta>,
134}
135
136impl Parse for BackendArg {
137    fn parse(input: ParseStream) -> syn::Result<Self> {
138        let id: Ident = input.parse()?;
139        let cfg = if input.peek(Token![:]) {
140            input.parse::<Token![:]>()?;
141
142            // This parses cfg(feature = "...") or any other meta item
143            let meta: syn::Meta = input.parse()?;
144            Some(meta)
145        } else {
146            None
147        };
148
149        Ok(Self { id, cfg })
150    }
151}
152
153impl Parse for Backends {
154    fn parse(input: ParseStream) -> syn::Result<Self> {
155        let args = Punctuated::<BackendArg, Token![,]>::parse_terminated(input)?;
156
157        let mut concrete = vec![];
158        let mut autodiff = (false, None);
159
160        for arg in args {
161            if arg.id == "Autodiff" {
162                autodiff = (true, arg.cfg);
163                continue;
164            }
165
166            concrete.push(Backend {
167                kind: BackendKind::try_from(&arg.id)?,
168                cfg: arg.cfg,
169            });
170        }
171
172        Ok(Backends { concrete, autodiff })
173    }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum TensorKind {
178    Float,
179    Int,
180    Bool,
181    Quantized,
182}
183
184#[allow(clippy::large_enum_variant)]
185enum ArgKind {
186    Tensor(TensorKind),
187    // A custom struct or enum of tensor primitives, marked `#[extension_type]` on the argument.
188    // Unwrapped back into the concrete backend's `Ty<B>` before the backend call via `ExtensionType`.
189    Extension(Type),
190    // Passthrough - unhandled by the macro
191    Other(Type),
192}
193
194struct OperationArg {
195    name: Ident,
196    kind: ArgKind,
197}
198
199#[allow(clippy::large_enum_variant)]
200#[derive(Debug, Clone)]
201enum OutputKind {
202    Tensor(TensorKind),
203    Custom(Type),
204}
205
206#[allow(clippy::large_enum_variant)]
207#[derive(Debug, Clone)]
208enum OperationOutput {
209    Tensor(TensorKind),
210    Tuple(Vec<OutputKind>),
211    Custom(Type),
212}
213
214struct Operation {
215    name: Ident,
216    inputs: Vec<OperationArg>,
217    output: OperationOutput,
218    asyncness: bool,
219}
220
221struct Extension {
222    trait_name: Ident,
223    backends: Backends,
224    ops: Vec<Operation>,
225}
226
227impl TensorKind {
228    fn from_type(ty: &Type) -> Option<Self> {
229        match ty {
230            // Handle `<Self as Backend>::<Primitive>`
231            Type::Path(tp) if tp.qself.is_some() => {
232                let last = tp.path.segments.last()?.ident.to_string();
233
234                match last.as_str() {
235                    "FloatTensorPrimitive" => Some(Self::Float),
236                    "IntTensorPrimitive" => Some(Self::Int),
237                    "BoolTensorPrimitive" => Some(Self::Bool),
238                    "QuantizedTensorPrimitive" => Some(Self::Quantized),
239                    _ => None,
240                }
241            }
242            // Handle simple paths: Float, FloatTensor<Self>, burn::...::FloatTensor<Self>
243            Type::Path(tp) => {
244                let last = tp.path.segments.last()?.ident.to_string();
245
246                match last.as_str() {
247                    // Shorthand
248                    "Float" => Some(Self::Float),
249                    "Int" => Some(Self::Int),
250                    "Bool" => Some(Self::Bool),
251                    "Quantized" => Some(Self::Quantized),
252
253                    // Full tensor types
254                    "FloatTensor" => Some(Self::Float),
255                    "IntTensor" => Some(Self::Int),
256                    "BoolTensor" => Some(Self::Bool),
257                    "QuantizedTensor" => Some(Self::Quantized),
258
259                    // Associated primitive types
260                    "FloatTensorPrimitive" => Some(Self::Float),
261                    "IntTensorPrimitive" => Some(Self::Int),
262                    "BoolTensorPrimitive" => Some(Self::Bool),
263                    "QuantizedTensorPrimitive" => Some(Self::Quantized),
264
265                    _ => None,
266                }
267            }
268
269            // Handle references like &FloatTensor<Self>
270            Type::Reference(r) => Self::from_type(&r.elem),
271
272            // Handle parentheses `(FloatTensor<Self>)`
273            Type::Paren(p) => Self::from_type(&p.elem),
274
275            // TODO: option/containers already handled?
276            _ => None,
277        }
278    }
279
280    fn to_primitive_ty(self) -> TokenStream2 {
281        match self {
282            Self::Float => quote! { burn::backend::tensor::FloatTensor<Self> },
283            Self::Int => quote! { burn::backend::tensor::IntTensor<Self> },
284            Self::Bool => quote! { burn::backend::tensor::BoolTensor<Self> },
285            Self::Quantized => quote! { burn::backend::tensor::QuantizedTensor<Self> },
286        }
287    }
288
289    fn variant(self) -> Ident {
290        match self {
291            Self::Float => format_ident!("Float"),
292            Self::Int => format_ident!("Int"),
293            Self::Bool => format_ident!("Bool"),
294            Self::Quantized => format_ident!("Quantized"),
295        }
296    }
297
298    fn unwrap_method(self) -> Ident {
299        // e.g. tensor.float() (BackendTensor method)
300        format_ident!("{}", format!("{:?}", self).to_lowercase())
301    }
302}
303
304fn backend_to_ident(b: &Backend) -> Ident {
305    // Convert the enum variant to a string first, then to an Ident
306    format_ident!("{}", format!("{:?}", b.kind))
307}
308
309fn extract_future_output_type(ty: &Type) -> Option<&Type> {
310    if let Type::ImplTrait(impl_trait) = ty {
311        for bound in &impl_trait.bounds {
312            if let TypeParamBound::Trait(trait_bound) = bound {
313                let last_segment = trait_bound.path.segments.last()?;
314                if last_segment.ident == "Future"
315                    && let PathArguments::AngleBracketed(args) = &last_segment.arguments
316                {
317                    for arg in &args.args {
318                        if let GenericArgument::AssocType(assoc) = arg
319                            && assoc.ident == "Output"
320                        {
321                            return Some(&assoc.ty);
322                        }
323                    }
324                }
325            }
326        }
327    }
328    None
329}
330
331fn lower_extension(attr: Backends, item: &ItemTrait) -> syn::Result<Extension> {
332    let mut ops = Vec::new();
333
334    for trait_item in &item.items {
335        let TraitItem::Fn(f) = trait_item else {
336            continue;
337        };
338
339        // Parse Inputs
340        let mut inputs = Vec::new();
341        for arg in &f.sig.inputs {
342            let FnArg::Typed(pt) = arg else { continue };
343            let name = match pt.pat.as_ref() {
344                Pat::Ident(p) => p.ident.clone(),
345                _ => return Err(syn::Error::new_spanned(&pt.pat, "Unsupported pattern")),
346            };
347            // An argument annotated with `#[extension_type]` is a custom struct or enum of tensor
348            // primitives (the input counterpart of the `#[derive(ExtensionType)]` output path).
349            let is_ext = pt
350                .attrs
351                .iter()
352                .any(|attr| attr.path().is_ident("extension_type"));
353            let kind = if is_ext {
354                validate_extension_ty(&pt.ty)?;
355                ArgKind::Extension((*pt.ty).clone())
356            } else if let Some(k) = TensorKind::from_type(&pt.ty) {
357                ArgKind::Tensor(k)
358            } else {
359                ArgKind::Other((*pt.ty).clone())
360            };
361            inputs.push(OperationArg { name, kind });
362        }
363
364        // Parse outputs
365        let (actual_ty, is_async) = match &f.sig.output {
366            ReturnType::Default => {
367                return Err(syn::Error::new_spanned(
368                    &f.sig.output,
369                    "Operations must return a value",
370                ));
371            }
372            ReturnType::Type(_, ty) => {
373                // If it's `impl Future<Output = T>`, extract T and mark as async.
374                // Otherwise, use the type as-is and check for `async fn`.
375                if let Some(out_ty) = extract_future_output_type(ty) {
376                    (out_ty, true)
377                } else {
378                    (ty.as_ref(), f.sig.asyncness.is_some())
379                }
380            }
381        };
382
383        let output = match actual_ty {
384            // TODO: expand support for vec and maybe nested containers
385            Type::Tuple(tup) => {
386                let elements = tup
387                    .elems
388                    .iter()
389                    .map(|elem| {
390                        if let Some(kind) = TensorKind::from_type(elem) {
391                            Ok(OutputKind::Tensor(kind))
392                        } else {
393                            Ok(OutputKind::Custom(elem.clone()))
394                        }
395                    })
396                    .collect::<syn::Result<Vec<_>>>()?;
397                OperationOutput::Tuple(elements)
398            }
399            ty if TensorKind::from_type(ty).is_some() => {
400                OperationOutput::Tensor(TensorKind::from_type(ty).unwrap())
401            }
402            ty => {
403                // ExtensionType
404                OperationOutput::Custom(ty.clone())
405            }
406        };
407
408        ops.push(Operation {
409            name: f.sig.ident.clone(),
410            inputs,
411            output,
412            asyncness: is_async,
413        });
414    }
415
416    Ok(Extension {
417        trait_name: item.ident.clone(),
418        backends: attr,
419        ops,
420    })
421}
422
423fn expand_extension(ir: Extension, mut original_trait: ItemTrait) -> TokenStream2 {
424    let trait_name = &ir.trait_name;
425
426    // `#[extension_type]` is a helper attribute understood only by this macro. Strip it from the
427    // argument list before re-emitting the trait, otherwise rustc rejects it as an unknown attribute.
428    for item in &mut original_trait.items {
429        if let TraitItem::Fn(f) = item {
430            for arg in &mut f.sig.inputs {
431                if let FnArg::Typed(pt) = arg {
432                    pt.attrs
433                        .retain(|attr| !attr.path().is_ident("extension_type"));
434                }
435            }
436        }
437    }
438
439    // Generate Dispatch Implementation
440    let dispatch_methods = ir.ops.iter().map(|op| gen_dispatch_method(&ir, op));
441
442    quote! {
443        #original_trait
444
445        impl #trait_name for burn::backend::Dispatch {
446            #( #dispatch_methods )*
447        }
448    }
449}
450
451fn gen_dispatch_method(ir: &Extension, op: &Operation) -> TokenStream2 {
452    let name = &op.name;
453    let has_ad = ir.backends.autodiff.0;
454
455    let maybe_async = if op.asyncness {
456        quote! { async }
457    } else {
458        quote! {}
459    };
460
461    let sig_args: Vec<_> = op
462        .inputs
463        .iter()
464        .map(|arg| {
465            let name = &arg.name;
466            match &arg.kind {
467                ArgKind::Tensor(k) => {
468                    let ty = k.to_primitive_ty();
469                    quote! { #name: #ty }
470                }
471                // Keep the original `Struct<Self>` type. Inside `impl Trait for Dispatch`, `Self`
472                // resolves to `Dispatch`, so the incoming value is the dispatch form `Struct<Dispatch>`.
473                ArgKind::Extension(ty) => quote! { #name: #ty },
474                ArgKind::Other(ty) => quote! { #name: #ty },
475            }
476        })
477        .collect();
478
479    let ret_ty = match &op.output {
480        OperationOutput::Tensor(k) => k.to_primitive_ty(),
481        OperationOutput::Tuple(elems) => {
482            let types = elems.iter().map(|e| match e {
483                OutputKind::Tensor(k) => k.to_primitive_ty(),
484                OutputKind::Custom(ty) => quote! { #ty },
485            });
486            quote! { (#(#types),*) }
487        }
488        OperationOutput::Custom(ty) => quote! { #ty },
489    };
490
491    let has_tensor_input = op
492        .inputs
493        .iter()
494        .any(|a| matches!(a.kind, ArgKind::Tensor(_)));
495
496    let has_ext_input = op
497        .inputs
498        .iter()
499        .any(|a| matches!(a.kind, ArgKind::Extension(_)));
500
501    let body = if !has_tensor_input && !has_ext_input {
502        // No tensor input to select the backend from (e.g. `fn load_data(i: usize) -> FloatTensor`).
503        // There is nothing to match on, so this is only well-defined for a single backend — the
504        // remote backend is the motivating case (`#[backend_extension(Remote)]`), where the op is
505        // shipped to the server. Dispatch directly to that backend; reject the ambiguous cases.
506        if has_ad {
507            quote! { compile_error!("A backend extension operation with no tensor inputs can't be combined with `Autodiff` — there is no input tensor to carry the autodiff graph.") }
508        } else if ir.backends.concrete.len() == 1 {
509            let backend = &ir.backends.concrete[0];
510            let call = gen_backend_call(ir, op, backend);
511            match &backend.cfg {
512                // Ungated backend: dispatch straight to it.
513                None => quote! { let checkpointing = None; #call },
514                // The single backend is `cfg`-gated. Mirror the match path: gate the call on the
515                // backend's cfg and fall back to `unimplemented!` when it's compiled out, so the
516                // method still has a valid body instead of referencing a backend that doesn't
517                // exist.
518                Some(meta) => quote! {
519                    match () {
520                        #[#meta]
521                        () => { let checkpointing = None; #call }
522                        #[allow(unreachable_patterns)]
523                        _ => unimplemented!("Backend not supported for custom op `{}`", stringify!(#name)),
524                    }
525                },
526            }
527        } else {
528            quote! { compile_error!("A backend extension operation with no tensor inputs must list exactly one backend (e.g. `#[backend_extension(Remote)]`), since there is no input tensor to select the backend from.") }
529        }
530    } else {
531        // Select the concrete backend by peeking through the dispatch representation before moving
532        // any inputs. This is required for autodiff float tensors, whose backend identity is nested
533        // inside `DispatchTensorKind::Autodiff`, and also supports extension structs and enums.
534        gen_tensor_input_dispatch_body(ir, op)
535    };
536
537    quote! {
538        #maybe_async fn #name(#(#sig_args),*) -> #ret_ty {
539            #body
540        }
541    }
542}
543
544/// Rewrite a struct type's single generic argument to `param`, e.g. `MyStruct<Self>` -> `MyStruct<Wgpu>`.
545/// Used to name the concrete `Struct<B>` (and the dispatch `Struct<Dispatch>`) from the `Struct<Self>`
546/// written in the trait signature.
547fn struct_ty_with_param(ty: &Type, param: TokenStream2) -> TokenStream2 {
548    if let Type::Path(tp) = ty {
549        let mut path = tp.path.clone();
550        if let Some(last) = path.segments.last_mut() {
551            last.arguments = PathArguments::None;
552        }
553        quote! { #path<#param> }
554    } else {
555        // Not a path type; leave it untouched and let rustc surface a clear error.
556        quote! { #ty }
557    }
558}
559
560/// Validate the type of a `#[extension_type]`-marked argument, emitting a clear `compile_error!` for
561/// the common misuses instead of letting them surface as obscure trait/type errors deeper in codegen.
562///
563/// The argument must be a struct or enum with exactly one generic backend parameter (`MyType<Self>`),
564/// since [`struct_ty_with_param`] rewrites that single parameter to the backend when unwrapping.
565/// Marking a bare tensor, a non-path type, or a multi-parameter type is rejected here.
566fn validate_extension_ty(ty: &Type) -> syn::Result<()> {
567    if TensorKind::from_type(ty).is_some() {
568        return Err(syn::Error::new_spanned(
569            ty,
570            "`#[extension_type]` marks a struct or enum of tensor primitives, not a tensor argument. \
571             Remove the attribute to pass a plain tensor.",
572        ));
573    }
574
575    let Type::Path(tp) = ty else {
576        return Err(syn::Error::new_spanned(
577            ty,
578            "`#[extension_type]` requires a struct or enum type with a single generic backend \
579             parameter, e.g. `MyType<Self>`.",
580        ));
581    };
582
583    let last = tp.path.segments.last().ok_or_else(|| {
584        syn::Error::new_spanned(
585            ty,
586            "`#[extension_type]` type must be a named struct or enum",
587        )
588    })?;
589    let type_args = match &last.arguments {
590        PathArguments::AngleBracketed(args) => args
591            .args
592            .iter()
593            .filter(|a| matches!(a, GenericArgument::Type(_)))
594            .count(),
595        _ => 0,
596    };
597    if type_args != 1 {
598        return Err(syn::Error::new_spanned(
599            ty,
600            "`#[extension_type]` type must have exactly one generic backend parameter, e.g. \
601             `MyType<Self>`.",
602        ));
603    }
604
605    Ok(())
606}
607
608/// `panic!` emitted in a concrete-path unwrap arm that a valid single-backend call never reaches: it
609/// can only be hit by a genuine backend mismatch or by mixed autodiff tracking across inputs.
610fn panic_backend_or_tracking_mismatch() -> TokenStream2 {
611    quote! {
612        panic!(
613            "backend extension op received tensor inputs on mismatched backends, or mixed autodiff-tracked and untracked float tensors; all tensor inputs must share one backend and tracking"
614        )
615    }
616}
617
618/// `panic!` emitted in an autodiff unwrap arm, only reachable on a genuine backend mismatch.
619fn panic_backend_mismatch() -> TokenStream2 {
620    quote! {
621        panic!(
622            "backend extension op received tensor inputs on mismatched backends; all tensor inputs must be on the same backend"
623        )
624    }
625}
626
627/// Generate the dispatch body for an operation with tensor-bearing inputs. General "peek then
628/// unwrap" path: handles any mix of bare tensors and extension structs/enums (including several),
629/// for both concrete backends and (when `Autodiff` is listed) the autodiff wrapper.
630///
631/// Backend identity is nested inside `DispatchTensorKind::Autodiff` for tracked float tensors, and
632/// extension structs can't be destructured in a dispatch-kind match. Therefore we:
633/// 1. Peek a representative [`DispatchTensor`] (a bare tensor itself, or a struct's via
634///    `ExtensionType::dispatch_repr`) and fold its `.kind` into a `(is_autodiff, backend_index)`
635///    tag. Folding to a small value drops the borrow before any input is moved.
636/// 2. In the matched arm, unwrap every input for the selected backend and re-wrap the output.
637///
638/// For an autodiff arm the target backend is `Autodiff<B>`: float tensors/fields unwrap through the
639/// `Autodiff(Box(#b(BackendTensor::Autodiff(_))))` nesting into `FloatTensor<Autodiff<B>>`, while
640/// int/bool/quantized ones stay plain (autodiff only tracks floats). The op's own
641/// `impl ... for Autodiff<B>` still hand-writes the backward pass exactly as for a bare-tensor
642/// autodiff op; the macro only routes and re-wraps.
643fn gen_tensor_input_dispatch_body(ir: &Extension, op: &Operation) -> TokenStream2 {
644    let name = &op.name;
645    let mismatch = panic_backend_or_tracking_mismatch();
646    let has_ad = ir.backends.autodiff.0;
647    // cfg gating the `Autodiff` entry itself (e.g. `Autodiff: cfg(feature = "autodiff")`). Every
648    // generated autodiff arm must carry it, mirroring the pure-tensor path, so the arms vanish when
649    // autodiff is compiled out. Otherwise their `DispatchTensorKind::Autodiff` / `Autodiff<B>`
650    // references (themselves feature-gated) would fail to compile.
651    let ad_cfg_attr = ir
652        .backends
653        .autodiff
654        .1
655        .as_ref()
656        .map(|meta| quote! { #[#meta] });
657
658    // Backend selection walks all inputs at runtime, preferring a *float* tensor (floats carry the
659    // autodiff tracking that decides the concrete-vs-autodiff arm, and the checkpointing strategy)
660    // and falling back to any tensor. `dispatch_repr` / `dispatch_float_repr` recurse into structs
661    // and enums and return `None` for a tensor-less value (e.g. an enum on a tensor-less variant),
662    // so the walk simply defers to the next input. If no input carries a tensor at runtime the
663    // backend is unresolvable and the op panics.
664    let repr_option = |float_only: bool| -> Vec<TokenStream2> {
665        op.inputs
666            .iter()
667            .filter_map(|a| match &a.kind {
668                ArgKind::Tensor(k) => {
669                    if float_only && *k != TensorKind::Float {
670                        return None;
671                    }
672                    let n = &a.name;
673                    Some(quote! { Some(&#n) })
674                }
675                ArgKind::Extension(ty) => {
676                    let n = &a.name;
677                    let target_ty = struct_ty_with_param(ty, quote! { burn::backend::Dispatch });
678                    let method = if float_only {
679                        quote! { dispatch_float_repr }
680                    } else {
681                        quote! { dispatch_repr }
682                    };
683                    Some(quote! {
684                        <#target_ty as burn::backend::ExtensionType<burn::backend::Dispatch>>::#method(&#n)
685                    })
686                }
687                ArgKind::Other(_) => None,
688            })
689            .collect()
690    };
691    // Chain the per-input options lazily: the first `Some` wins, later `dispatch_repr` calls run only
692    // if earlier inputs had no tensor.
693    let chain = |opts: Vec<TokenStream2>| -> TokenStream2 {
694        match opts.split_first() {
695            None => quote! { Option::<&burn::backend::DispatchTensor>::None },
696            Some((first, rest)) => quote! { #first #( .or_else(|| #rest) )* },
697        }
698    };
699    let float_chain = chain(repr_option(true));
700    let any_chain = chain(repr_option(false));
701
702    // Tag arms fold the representative kind into `(is_autodiff, backend_index)`. cfg-gated backends
703    // drop the same arm from both the tag match and the dispatch match, keeping indices aligned.
704    let concrete_tag_arms = ir.backends.concrete.iter().enumerate().map(|(i, backend)| {
705        let cfg_attr = backend.cfg.as_ref().map(|meta| quote! { #[#meta] });
706        let b_ident = backend_to_ident(backend);
707        quote! {
708            #cfg_attr
709            burn::backend::DispatchTensorKind::#b_ident(_) => (false, #i),
710        }
711    });
712    let ad_tag_arm = has_ad.then(|| {
713        let inner = ir.backends.concrete.iter().enumerate().map(|(i, backend)| {
714            let cfg_attr = backend.cfg.as_ref().map(|meta| quote! { #[#meta] });
715            let b_ident = backend_to_ident(backend);
716            quote! {
717                #cfg_attr
718                burn::backend::DispatchTensorKind::#b_ident(_) => #i,
719            }
720        });
721        quote! {
722            #ad_cfg_attr
723            burn::backend::DispatchTensorKind::Autodiff(inner) => (true, match inner.as_ref() {
724                #( #inner )*
725                #[allow(unreachable_patterns)]
726                _ => usize::MAX,
727            }),
728        }
729    });
730
731    let concrete_call_arms = ir.backends.concrete.iter().enumerate().map(|(i, backend)| {
732        let cfg_attr = backend.cfg.as_ref().map(|meta| quote! { #[#meta] });
733        let b_ident = backend_to_ident(backend);
734
735        // Pull each bare tensor out of its `DispatchTensor` into a `BackendTensor<B>`, matching what
736        // the pure-tensor path binds in its pattern. `gen_backend_call` then applies `.float()`/etc.
737        let pre_extract = op.inputs.iter().filter_map(|a| match &a.kind {
738            ArgKind::Tensor(_) => {
739                let n = &a.name;
740                Some(quote! {
741                    let #n = match #n.kind {
742                        burn::backend::DispatchTensorKind::#b_ident(bt) => bt,
743                        #[allow(unreachable_patterns)]
744                        _ => #mismatch,
745                    };
746                })
747            }
748            _ => None,
749        });
750
751        let call = gen_backend_call(ir, op, backend);
752        quote! {
753            #cfg_attr
754            (false, #i) => {
755                #( #pre_extract )*
756                #call
757            }
758        }
759    });
760
761    let ad_call_arms: Vec<_> = if has_ad {
762        ir.backends
763            .concrete
764            .iter()
765            .enumerate()
766            .map(|(i, backend)| gen_tensor_input_ad_arm(ir, op, backend, i, &ad_cfg_attr))
767            .collect()
768    } else {
769        Vec::new()
770    };
771
772    quote! {
773        // Compute the checkpointing strategy and backend tag in a scoped block so the representative's
774        // borrow of the inputs ends before the dispatch arms below move them.
775        let (checkpointing, __burn_backend_tag): (
776            Option<burn::backend::CheckpointingStrategy>,
777            (bool, usize),
778        ) = {
779            let __repr: &burn::backend::DispatchTensor = (#float_chain)
780                .or_else(|| #any_chain)
781                .expect("backend extension op received no tensor input to select a backend from (e.g. an enum input on a tensor-less variant with no other tensor input)");
782            (
783                __repr.checkpointing.clone(),
784                match &__repr.kind {
785                    #ad_tag_arm
786                    #( #concrete_tag_arms )*
787                    #[allow(unreachable_patterns)]
788                    _ => (false, usize::MAX),
789                },
790            )
791        };
792        match __burn_backend_tag {
793            #( #concrete_call_arms )*
794            #( #ad_call_arms )*
795            _ => unimplemented!("Backend not supported for custom op `{}`", stringify!(#name)),
796        }
797    }
798}
799
800/// Generate a single `(true, backend_index)` autodiff arm for the tensor dispatch path: unwrap every
801/// input into its `Autodiff<B>` primitive, call `<Autodiff<B> as Trait>::op`, and re-wrap the output.
802fn gen_tensor_input_ad_arm(
803    ir: &Extension,
804    op: &Operation,
805    backend: &Backend,
806    i: usize,
807    ad_cfg_attr: &Option<TokenStream2>,
808) -> TokenStream2 {
809    let cfg_attr = backend.cfg.as_ref().map(|meta| quote! { #[#meta] });
810    let b_ident = backend_to_ident(backend);
811    let trait_name = &ir.trait_name;
812    let fn_name = &op.name;
813    let mismatch = panic_backend_mismatch();
814    let maybe_await = if op.asyncness {
815        quote! { .await }
816    } else {
817        quote! {}
818    };
819
820    let unwraps = op.inputs.iter().filter_map(|a| match &a.kind {
821        // Float tensor: peel the autodiff nesting to recover `FloatTensor<Autodiff<B>>`.
822        ArgKind::Tensor(TensorKind::Float) => {
823            let n = &a.name;
824            Some(quote! {
825                let #n = match #n.kind {
826                    burn::backend::DispatchTensorKind::Autodiff(inner) => match *inner {
827                        burn::backend::DispatchTensorKind::#b_ident(bt) => bt.autodiff(),
828                        #[allow(unreachable_patterns)]
829                        _ => #mismatch,
830                    },
831                    #[allow(unreachable_patterns)]
832                    _ => panic!(
833                        "backend extension op mixes autodiff-tracked and untracked float tensors; all float inputs must share the same tracking"
834                    ),
835                };
836            })
837        }
838        // Int/bool/quantized tensor: not autodiff-tracked, unwrap as usual.
839        ArgKind::Tensor(kind) => {
840            let n = &a.name;
841            let method = kind.unwrap_method();
842            Some(quote! {
843                let #n = match #n.kind {
844                    burn::backend::DispatchTensorKind::#b_ident(bt) => bt.#method(),
845                    #[allow(unreachable_patterns)]
846                    _ => unreachable!("tensor input routed to the wrong backend"),
847                };
848            })
849        }
850        // Struct/enum: reconstruct `Ty<Autodiff<B>>`. The closure pulls each field's `BackendTensor`
851        // out of its dispatch kind (float fields arrive autodiff-wrapped, others plain) and lifts it
852        // to `Autodiff<B>` via `into_autodiff`.
853        ArgKind::Extension(ty) => {
854            let n = &a.name;
855            let ad_ty = struct_ty_with_param(ty, quote! { Autodiff<#b_ident> });
856            Some(quote! {
857                let #n = <#ad_ty as burn::backend::ExtensionType<Autodiff<#b_ident>>>::map_from_dispatch(
858                    #n,
859                    |kind| {
860                        let bt = match kind {
861                            burn::backend::DispatchTensorKind::Autodiff(inner) => match *inner {
862                                burn::backend::DispatchTensorKind::#b_ident(bt) => bt,
863                                #[allow(unreachable_patterns)]
864                                _ => #mismatch,
865                            },
866                            burn::backend::DispatchTensorKind::#b_ident(bt) => bt,
867                            #[allow(unreachable_patterns)]
868                            _ => #mismatch,
869                        };
870                        bt.into_autodiff()
871                    },
872                );
873            })
874        }
875        ArgKind::Other(_) => None,
876    });
877
878    let call_args = op.inputs.iter().map(|a| &a.name);
879    let wrap_out = gen_output_wrap(op, &b_ident, true);
880
881    quote! {
882        #ad_cfg_attr
883        #cfg_attr
884        (true, #i) => {
885            #( #unwraps )*
886            type _ADBackend = Autodiff<#b_ident>;
887            let _out = <_ADBackend as #trait_name>::#fn_name(#( #call_args ),*)#maybe_await;
888            #wrap_out
889        }
890    }
891}
892
893/// Wrap the backend call's result `_out` back into the dispatch representation. Shared by every
894/// dispatch arm (concrete and autodiff). `is_ad` toggles the autodiff nesting: float outputs of an
895/// `Autodiff<B>` op are re-wrapped as `Autodiff(Box(#b(BackendTensor::Autodiff(_))))`, while int/bool/
896/// quantized outputs stay plain (autodiff only tracks floats).
897fn gen_output_wrap(op: &Operation, b_ident: &Ident, is_ad: bool) -> TokenStream2 {
898    // Wrap a single custom (`ExtensionType`) output value read via `accessor`.
899    let wrap_custom = |accessor: TokenStream2| -> TokenStream2 {
900        if is_ad {
901            quote! {
902                burn::backend::ExtensionType::map_to_dispatch(
903                    #accessor,
904                    |tensor| match tensor {
905                        burn::backend::BackendTensor::Float(t) => burn::backend::DispatchTensorKind::Autodiff(
906                            Box::new(burn::backend::DispatchTensorKind::#b_ident(
907                                burn::backend::BackendTensor::Autodiff(t),
908                            )),
909                        ),
910                        burn::backend::BackendTensor::Int(t) => burn::backend::DispatchTensorKind::#b_ident(burn::backend::BackendTensor::Int(t)),
911                        burn::backend::BackendTensor::Bool(t) => burn::backend::DispatchTensorKind::#b_ident(burn::backend::BackendTensor::Bool(t)),
912                        burn::backend::BackendTensor::Quantized(t) => burn::backend::DispatchTensorKind::#b_ident(burn::backend::BackendTensor::Quantized(t)),
913                        #[allow(unreachable_patterns)]
914                        _ => unreachable!("unexpected output tensor variant"),
915                    },
916                    checkpointing,
917                )
918            }
919        } else {
920            quote! {
921                burn::backend::ExtensionType::map_to_dispatch(
922                    #accessor,
923                    |tensor| burn::backend::DispatchTensorKind::#b_ident(tensor),
924                    checkpointing,
925                )
926            }
927        }
928    };
929
930    match &op.output {
931        OperationOutput::Tensor(kind) => {
932            let wrapped = gen_tensor_wrap(kind, quote! { _out }, b_ident, is_ad);
933            quote! { burn::backend::DispatchTensor { kind: #wrapped, checkpointing } }
934        }
935        OperationOutput::Tuple(elems) => {
936            let elements = elems.iter().enumerate().map(|(i, elem)| {
937                let idx = syn::Index::from(i);
938                match elem {
939                    OutputKind::Tensor(kind) => {
940                        let wrapped = gen_tensor_wrap(kind, quote! { _out.#idx }, b_ident, is_ad);
941                        quote! { burn::backend::DispatchTensor { kind: #wrapped, checkpointing } }
942                    }
943                    OutputKind::Custom(_) => wrap_custom(quote! { _out.#idx }),
944                }
945            });
946            quote! { (#(#elements),*) }
947        }
948        OperationOutput::Custom(_) => wrap_custom(quote! { _out }),
949    }
950}
951
952/// Generate the body that unwraps the dispatch tensors, calls the backend's trait impl and wraps the
953/// result back into a [`DispatchTensor`]. Used by the concrete dispatch arms and the direct
954/// no-tensor-input dispatch path.
955fn gen_backend_call(ir: &Extension, op: &Operation, backend: &Backend) -> TokenStream2 {
956    let b_ident = backend_to_ident(backend);
957    let trait_name = &ir.trait_name;
958    let fn_name = &op.name;
959    let mismatch = panic_backend_or_tracking_mismatch();
960
961    // Unwrap inner kind: lhs.float(), rhs.int(), etc. (no-op when there are no tensor inputs).
962    let unwraps = op.inputs.iter().filter_map(|a| match &a.kind {
963        ArgKind::Tensor(kind) => {
964            let name = &a.name;
965            let method = kind.unwrap_method();
966            Some(quote! { let #name = #name.#method(); })
967        }
968        // Reconstruct `Struct<B>` from the incoming `Struct<Dispatch>` by unwrapping each tensor
969        // field into this backend's `BackendTensor`. Inverse of the `map_to_dispatch` output path.
970        ArgKind::Extension(ty) => {
971            let name = &a.name;
972            let b_ty = struct_ty_with_param(ty, quote! { #b_ident });
973            Some(quote! {
974                let #name = <#b_ty as burn::backend::ExtensionType<#b_ident>>::map_from_dispatch(
975                    #name,
976                    |kind| match kind {
977                        burn::backend::DispatchTensorKind::#b_ident(bt) => bt,
978                        #[allow(unreachable_patterns)]
979                        _ => #mismatch,
980                    },
981                );
982            })
983        }
984        _ => None,
985    });
986
987    let call_args = op.inputs.iter().map(|a| &a.name);
988    let maybe_await = if op.asyncness {
989        quote! { .await }
990    } else {
991        quote! {}
992    };
993
994    let wrap_out = gen_output_wrap(op, &b_ident, false);
995
996    quote! {
997        #(#unwraps)*
998        let _out = <#b_ident as #trait_name>::#fn_name(#(#call_args),*)#maybe_await;
999        #wrap_out
1000    }
1001}
1002
1003fn gen_tensor_wrap(
1004    kind: &TensorKind,
1005    val: TokenStream2,
1006    b_ident: &Ident,
1007    is_ad: bool,
1008) -> TokenStream2 {
1009    let variant = kind.variant();
1010    if is_ad && *kind == TensorKind::Float {
1011        quote! {
1012            burn::backend::DispatchTensorKind::Autodiff(
1013                Box::new(burn::backend::DispatchTensorKind::#b_ident(
1014                    burn::backend::BackendTensor::Autodiff(#val)
1015                ))
1016            )
1017        }
1018    } else {
1019        quote! {
1020            burn::backend::DispatchTensorKind::#b_ident(
1021                burn::backend::BackendTensor::#variant(#val)
1022            )
1023        }
1024    }
1025}
1026
1027/// Field layout of a struct or a single enum variant.
1028#[derive(Clone, Copy, PartialEq, Eq)]
1029enum CaseStyle {
1030    Named,
1031    Unnamed,
1032    Unit,
1033}
1034
1035/// One derive "case": a struct is a single case; an enum contributes one case per variant. Unifies
1036/// struct and enum handling. The generated methods are a `match` over cases (trivial for a struct).
1037struct DeriveCase {
1038    /// Path to match / construct, e.g. `Name` or `Name::Variant`.
1039    path: TokenStream2,
1040    style: CaseStyle,
1041    fields: Vec<CaseField>,
1042}
1043
1044struct CaseField {
1045    /// Synthesized binding (`__ext_f{i}`), used for both named and tuple fields so the bindings never
1046    /// collide with method params like `map_kind` / `checkpointing`.
1047    bind: Ident,
1048    /// Field name for named fields; `None` for tuple fields (constructed positionally).
1049    member: Option<Ident>,
1050    ty: Type,
1051    is_ext: bool,
1052    tensor_kind: Option<TensorKind>,
1053}
1054
1055fn build_case(path: TokenStream2, fields: &Fields) -> DeriveCase {
1056    let (style, raw): (CaseStyle, Vec<&syn::Field>) = match fields {
1057        Fields::Named(f) => (CaseStyle::Named, f.named.iter().collect()),
1058        Fields::Unnamed(f) => (CaseStyle::Unnamed, f.unnamed.iter().collect()),
1059        Fields::Unit => (CaseStyle::Unit, Vec::new()),
1060    };
1061    let fields = raw
1062        .iter()
1063        .enumerate()
1064        .map(|(i, f)| CaseField {
1065            bind: format_ident!("__ext_f{}", i),
1066            member: f.ident.clone(),
1067            ty: f.ty.clone(),
1068            is_ext: f.attrs.iter().any(|a| a.path().is_ident("extension_type")),
1069            tensor_kind: TensorKind::from_type(&f.ty),
1070        })
1071        .collect();
1072    DeriveCase {
1073        path,
1074        style,
1075        fields,
1076    }
1077}
1078
1079fn collect_cases(input: &DeriveInput) -> syn::Result<Vec<DeriveCase>> {
1080    let name = &input.ident;
1081    match &input.data {
1082        Data::Struct(s) => Ok(vec![build_case(quote! { #name }, &s.fields)]),
1083        Data::Enum(e) => Ok(e
1084            .variants
1085            .iter()
1086            .map(|v| {
1087                let vident = &v.ident;
1088                build_case(quote! { #name::#vident }, &v.fields)
1089            })
1090            .collect()),
1091        Data::Union(_) => Err(syn::Error::new_spanned(
1092            name,
1093            "ExtensionType cannot be derived for unions",
1094        )),
1095    }
1096}
1097
1098/// Destructuring pattern binding the fields for which `needed(i)` is true and `_`-ignoring the rest.
1099/// The same pattern serves an owned scrutinee (`self` / `target`) and a reference (`&target`); match
1100/// ergonomics binds by reference in the latter.
1101fn gen_case_pattern(case: &DeriveCase, needed: impl Fn(usize) -> bool) -> TokenStream2 {
1102    let path = &case.path;
1103    match case.style {
1104        CaseStyle::Unit => quote! { #path },
1105        CaseStyle::Named => {
1106            let entries = case.fields.iter().enumerate().map(|(i, f)| {
1107                let member = f.member.as_ref().expect("named field has an ident");
1108                if needed(i) {
1109                    let bind = &f.bind;
1110                    quote! { #member: #bind }
1111                } else {
1112                    quote! { #member: _ }
1113                }
1114            });
1115            quote! { #path { #( #entries ),* } }
1116        }
1117        CaseStyle::Unnamed => {
1118            let entries = case.fields.iter().enumerate().map(|(i, f)| {
1119                if needed(i) {
1120                    let bind = &f.bind;
1121                    quote! { #bind }
1122                } else {
1123                    quote! { _ }
1124                }
1125            });
1126            quote! { #path ( #( #entries ),* ) }
1127        }
1128    }
1129}
1130
1131/// Reconstruct the case from one transformed expression per field (same order as `case.fields`).
1132fn gen_case_ctor(case: &DeriveCase, exprs: &[TokenStream2]) -> TokenStream2 {
1133    let path = &case.path;
1134    match case.style {
1135        CaseStyle::Unit => quote! { #path },
1136        CaseStyle::Named => {
1137            let entries = case.fields.iter().zip(exprs).map(|(f, e)| {
1138                let member = f.member.as_ref().expect("named field has an ident");
1139                quote! { #member: #e }
1140            });
1141            quote! { #path { #( #entries ),* } }
1142        }
1143        CaseStyle::Unnamed => quote! { #path ( #( #exprs ),* ) },
1144    }
1145}
1146
1147/// One `dispatch_repr` / `dispatch_float_repr` match arm for a case: bind only the field(s) the
1148/// representative needs and return an `Option<&DispatchTensor>`. Prefers a float tensor field, then
1149/// (unless `float_only`) any tensor field, then chains over nested `#[extension_type]` fields.
1150fn gen_repr_arm(case: &DeriveCase, float_only: bool) -> TokenStream2 {
1151    let float_i = case
1152        .fields
1153        .iter()
1154        .position(|f| !f.is_ext && f.tensor_kind == Some(TensorKind::Float));
1155    // Only relevant as a non-float fallback, i.e. never in the `float_only` expansion.
1156    let any_i = if float_only {
1157        None
1158    } else {
1159        case.fields
1160            .iter()
1161            .position(|f| !f.is_ext && f.tensor_kind.is_some())
1162    };
1163    let ext_is: Vec<usize> = case
1164        .fields
1165        .iter()
1166        .enumerate()
1167        .filter(|(_, f)| f.is_ext)
1168        .map(|(i, _)| i)
1169        .collect();
1170    let method = if float_only {
1171        format_ident!("dispatch_float_repr")
1172    } else {
1173        format_ident!("dispatch_repr")
1174    };
1175
1176    let (needed, expr): (Vec<usize>, TokenStream2) = if let Some(i) = float_i {
1177        let bind = &case.fields[i].bind;
1178        (vec![i], quote! { Some(#bind) })
1179    } else if let Some(i) = any_i {
1180        let bind = &case.fields[i].bind;
1181        (vec![i], quote! { Some(#bind) })
1182    } else if !ext_is.is_empty() {
1183        let calls = ext_is.iter().map(|&i| {
1184            let bind = &case.fields[i].bind;
1185            let dispatch_ty = struct_ty_with_param(&case.fields[i].ty, quote! { burn::backend::Dispatch });
1186            quote! { .or_else(|| <#dispatch_ty as burn::backend::ExtensionType<burn::backend::Dispatch>>::#method(#bind)) }
1187        });
1188        (
1189            ext_is.clone(),
1190            quote! { Option::<&burn::backend::DispatchTensor>::None #( #calls )* },
1191        )
1192    } else {
1193        (
1194            Vec::new(),
1195            quote! { Option::<&burn::backend::DispatchTensor>::None },
1196        )
1197    };
1198
1199    let pattern = gen_case_pattern(case, |i| needed.contains(&i));
1200    quote! { #pattern => #expr, }
1201}
1202
1203/// Derive macro to implement `ExtensionType` for custom structs and enums of tensor primitives,
1204/// letting them cross the `Dispatch` boundary as backend extension operation inputs or outputs.
1205///
1206/// # Requirements
1207///
1208/// - Applies to a `struct` (named or tuple fields) or an `enum` (any mix of named/tuple/unit
1209///   variants). Unions are unsupported.
1210/// - The type must be generic over a single `Backend` type parameter named `B`.
1211///
1212/// # Field attributes
1213///
1214/// Each field is inspected by type:
1215/// - **Tensor primitives** (`FloatTensor<B>`, `IntTensor<B>`, ...): mapped automatically.
1216/// - **Other types**: passed through unmodified.
1217///
1218/// To nest another `ExtensionType` struct/enum, annotate the field with `#[extension_type]` so the
1219/// macro traverses it recursively.
1220///
1221/// # Backend selection for inputs
1222///
1223/// When used as an input, the dispatch glue selects the backend from a representative tensor. For an
1224/// enum this depends on the active variant, and a tensor-less variant (or unit variant) yields no
1225/// representative, so the glue then walks the op's other inputs. If no input carries a tensor at
1226/// runtime the backend is unresolvable and the op panics.
1227///
1228/// # Example
1229///
1230/// ```rust,ignore
1231/// #[derive(ExtensionType)]
1232/// pub struct OperationOutput<B: Backend> {
1233///     pub bool: BoolTensor<B>,
1234///     pub int: IntTensor<B>,
1235///     pub float: FloatTensor<B>,
1236///     pub count: usize, // Non-tensor field passes through automatically
1237/// }
1238///
1239/// #[derive(ExtensionType)]
1240/// pub enum Input<B: Backend> {
1241///     Dense(FloatTensor<B>),
1242///     Sparse { values: FloatTensor<B>, indices: IntTensor<B> },
1243/// }
1244/// ```
1245#[proc_macro_derive(ExtensionType, attributes(extension_type))]
1246pub fn derive_extension_type(input: TokenStream) -> TokenStream {
1247    let input = parse_macro_input!(input as DeriveInput);
1248    let name = &input.ident;
1249    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1250
1251    let cases = match collect_cases(&input) {
1252        Ok(cases) => cases,
1253        Err(err) => return err.to_compile_error().into(),
1254    };
1255
1256    // `map_to_dispatch`: wrap every field's concrete primitive into a `DispatchTensor`.
1257    let wrap_arms = cases.iter().map(|case| {
1258        let pattern = gen_case_pattern(case, |_| true);
1259        let exprs: Vec<_> = case
1260            .fields
1261            .iter()
1262            .map(|f| {
1263                let bind = &f.bind;
1264                if f.is_ext {
1265                    quote! { #bind.map_to_dispatch(&map_kind, checkpointing) }
1266                } else if let Some(kind) = f.tensor_kind {
1267                    let variant = kind.variant();
1268                    quote! {
1269                        burn::backend::DispatchTensor {
1270                            kind: map_kind(burn::backend::BackendTensor::#variant(#bind)),
1271                            checkpointing,
1272                        }
1273                    }
1274                } else {
1275                    quote! { #bind }
1276                }
1277            })
1278            .collect();
1279        let ctor = gen_case_ctor(case, &exprs);
1280        quote! { #pattern => #ctor, }
1281    });
1282
1283    // `map_from_dispatch`: reverse of `wrap_arms`, recovering each concrete primitive.
1284    let unwrap_arms = cases.iter().map(|case| {
1285        let pattern = gen_case_pattern(case, |_| true);
1286        let exprs: Vec<_> = case
1287            .fields
1288            .iter()
1289            .map(|f| {
1290                let bind = &f.bind;
1291                if f.is_ext {
1292                    quote! { burn::backend::ExtensionType::map_from_dispatch(#bind, &unwrap_kind) }
1293                } else if let Some(kind) = f.tensor_kind {
1294                    let method = kind.unwrap_method();
1295                    quote! { unwrap_kind(#bind.kind).#method() }
1296                } else {
1297                    quote! { #bind }
1298                }
1299            })
1300            .collect();
1301        let ctor = gen_case_ctor(case, &exprs);
1302        quote! { #pattern => #ctor, }
1303    });
1304
1305    let any_repr_arms = cases.iter().map(|case| gen_repr_arm(case, false));
1306    let float_repr_arms = cases.iter().map(|case| gen_repr_arm(case, true));
1307
1308    // `#[allow(unused_variables)]` on the two map methods: `map_kind`/`unwrap_kind`/`checkpointing`
1309    // are genuinely unused for all-passthrough or unit types. The trade-off is that it also hides the
1310    // warning that would otherwise flag a field whose tensor type went unrecognized (e.g. written
1311    // through an opaque type alias) and was silently treated as a passthrough.
1312    TokenStream::from(quote! {
1313        impl #impl_generics burn::backend::ExtensionType<B> for #name #ty_generics #where_clause {
1314            type Target = #name<burn::backend::Dispatch>;
1315
1316            #[allow(unused_variables)]
1317            fn map_to_dispatch<F>(
1318                self,
1319                map_kind: F,
1320                checkpointing: Option<burn::backend::CheckpointingStrategy>,
1321            ) -> Self::Target
1322            where
1323                F: Fn(burn::backend::BackendTensor<B>) -> burn::backend::DispatchTensorKind,
1324            {
1325                match self { #( #wrap_arms )* }
1326            }
1327
1328            #[allow(unused_variables)]
1329            fn map_from_dispatch<F>(target: Self::Target, unwrap_kind: F) -> Self
1330            where
1331                F: Fn(burn::backend::DispatchTensorKind) -> burn::backend::BackendTensor<B>,
1332            {
1333                match target { #( #unwrap_arms )* }
1334            }
1335
1336            fn dispatch_repr(target: &Self::Target) -> Option<&burn::backend::DispatchTensor> {
1337                match target { #( #any_repr_arms )* }
1338            }
1339
1340            fn dispatch_float_repr(target: &Self::Target) -> Option<&burn::backend::DispatchTensor> {
1341                match target { #( #float_repr_arms )* }
1342            }
1343        }
1344    })
1345}