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#[proc_macro_attribute]
68pub fn backend_extension(attr: TokenStream, item: TokenStream) -> TokenStream {
69 let backends = parse_macro_input!(attr as Backends);
71 let trait_def = parse_macro_input!(item as ItemTrait);
73
74 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
130struct 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 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 Extension(Type),
190 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 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 Type::Path(tp) => {
244 let last = tp.path.segments.last()?.ident.to_string();
245
246 match last.as_str() {
247 "Float" => Some(Self::Float),
249 "Int" => Some(Self::Int),
250 "Bool" => Some(Self::Bool),
251 "Quantized" => Some(Self::Quantized),
252
253 "FloatTensor" => Some(Self::Float),
255 "IntTensor" => Some(Self::Int),
256 "BoolTensor" => Some(Self::Bool),
257 "QuantizedTensor" => Some(Self::Quantized),
258
259 "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 Type::Reference(r) => Self::from_type(&r.elem),
271
272 Type::Paren(p) => Self::from_type(&p.elem),
274
275 _ => 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 format_ident!("{}", format!("{:?}", self).to_lowercase())
301 }
302}
303
304fn backend_to_ident(b: &Backend) -> Ident {
305 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 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 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 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 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 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 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 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 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 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 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 None => quote! { let checkpointing = None; #call },
514 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 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
544fn 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 quote! { #ty }
557 }
558}
559
560fn 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
608fn 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
618fn 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
627fn 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 let ad_cfg_attr = ir
652 .backends
653 .autodiff
654 .1
655 .as_ref()
656 .map(|meta| quote! { #[#meta] });
657
658 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 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 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 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 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
800fn 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 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 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 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
893fn gen_output_wrap(op: &Operation, b_ident: &Ident, is_ad: bool) -> TokenStream2 {
898 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
952fn 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 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 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#[derive(Clone, Copy, PartialEq, Eq)]
1029enum CaseStyle {
1030 Named,
1031 Unnamed,
1032 Unit,
1033}
1034
1035struct DeriveCase {
1038 path: TokenStream2,
1040 style: CaseStyle,
1041 fields: Vec<CaseField>,
1042}
1043
1044struct CaseField {
1045 bind: Ident,
1048 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
1098fn 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
1131fn 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
1147fn 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 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#[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 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 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 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}