//! Error derive and reporting-attribute expansion for `kaptan-err-reportable`. use proc_macro::TokenStream; use quote::{format_ident, quote}; use std::collections::BTreeMap; use syn::{ Data, DeriveInput, Error, Fields, ItemFn, LitInt, ReturnType, Variant, parse_macro_input, parse_quote, spanned::Spanned, }; /// Implements `kaptan_err_reportable::ReportableError`, [`core::fmt::Display`], and /// [`core::error::Error`] for an explicitly numbered error enum. /// /// Every variant must declare a stable nonzero /// `#[kaptan_error(id = ...)]`. The derive rejects duplicates within an enum. /// Across enums, duplicate IDs emit the same reserved symbol. Collision detection /// requires both defining objects to participate in compilation or final linking. /// Unextracted archive members are not checked, so catalog allocation still needs /// review. The fixture guide in `tests/final_link.md` describes Rust release-LTO /// and native Linux checks. Embedded linker combinations need their own checks. #[proc_macro_derive(KaptanError, attributes(kaptan_error))] pub fn derive_kaptan_error(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); expand_error(input) .unwrap_or_else(Error::into_compile_error) .into() } /// Reports an [`Err`] only when it leaves the annotated function. /// /// The function must have an explicit return type compatible with /// [`core::result::Result`]; aliases are accepted. Const, unsafe, variadic, /// and foreign-ABI functions are rejected because an immediately invoked /// closure or async block cannot preserve those execution contracts without /// additional unsafe or non-const machinery. #[proc_macro_attribute] pub fn propagate(arguments: TokenStream, input: TokenStream) -> TokenStream { if !arguments.is_empty() { return Error::new( proc_macro2::Span::call_site(), "kaptan_err_reportable::propagate does not accept arguments", ) .into_compile_error() .into(); } let function = parse_macro_input!(input as ItemFn); expand(function) .unwrap_or_else(Error::into_compile_error) .into() } fn expand(mut function: ItemFn) -> syn::Result { validate_signature(&function)?; let body = function.block; function.block = if function.sig.asyncness.is_some() { Box::new(syn::parse_quote!({ const __KAPTAN_RAISED_AT: &str = concat!(file!(), ":", line!()); let __kaptan_err_reportable_result = (async #body).await; ::kaptan_err_reportable::__private::report_result( &__kaptan_err_reportable_result, __KAPTAN_RAISED_AT, ); __kaptan_err_reportable_result })) } else { Box::new(syn::parse_quote!({ const __KAPTAN_RAISED_AT: &str = concat!(file!(), ":", line!()); let __kaptan_err_reportable_result = (|| #body)(); ::kaptan_err_reportable::__private::report_result( &__kaptan_err_reportable_result, __KAPTAN_RAISED_AT, ); __kaptan_err_reportable_result })) }; Ok(quote!(#function)) } fn validate_signature(function: &ItemFn) -> syn::Result<()> { let signature = &function.sig; if let Some(token) = signature.constness { return Err(Error::new( token.span(), "kaptan_err_reportable::propagate does not support const functions", )); } if let Some(token) = signature.unsafety { return Err(Error::new( token.span(), "kaptan_err_reportable::propagate does not support unsafe functions", )); } if let Some(abi) = &signature.abi { return Err(Error::new( abi.span(), "kaptan_err_reportable::propagate does not support foreign-ABI functions", )); } if let Some(variadic) = &signature.variadic { return Err(Error::new( variadic.span(), "kaptan_err_reportable::propagate does not support variadic functions", )); } if matches!(signature.output, ReturnType::Default) { return Err(Error::new( signature.output.span(), "kaptan_err_reportable::propagate requires an explicit Result return type", )); } Ok(()) } fn expand_error(input: DeriveInput) -> syn::Result { let DeriveInput { ident, generics, data, .. } = input; let Data::Enum(data) = data else { let kind = match data { Data::Struct(_) => "structs", Data::Union(_) => "unions", Data::Enum(_) => unreachable!("enum handled above"), }; return Err(Error::new_spanned( ident, format!("KaptanError can only be derived for enums, not {kind}"), )); }; let mut ids = BTreeMap::new(); let variants = data .variants .iter() .map(|variant| { let id = variant_id(variant)?; if let Some(previous) = ids.insert(id, &variant.ident) { return Err(Error::new_spanned( &variant.ident, format!("Kaptan error ID {id} is already assigned to variant {previous}",), )); } Ok((variant, id)) }) .collect::>>()?; let identities = variants.iter().map(|(variant, id)| { let variant_ident = &variant.ident; let static_ident = format_ident!( "__KAPTAN_ERROR_ID_{}_{}", ident.to_string().to_uppercase(), variant_ident.to_string().to_uppercase(), ); let symbol = syn::LitStr::new(&format!("__kaptan_error_id_{id}"), variant_ident.span()); quote! { #[doc(hidden)] #[used] #[unsafe(export_name = #symbol)] static #static_ident: u8 = 0; } }); let arms = variants.iter().map(|(variant, id)| { let variant_ident = &variant.ident; let pattern = match &variant.fields { Fields::Unit => quote!(Self::#variant_ident), Fields::Unnamed(_) => quote!(Self::#variant_ident(..)), Fields::Named(_) => quote!(Self::#variant_ident { .. }), }; quote! { #pattern => ::kaptan_err_reportable::ErrorId::new(#id) } }); let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); let mut error_generics = generics.clone(); error_generics .make_where_clause() .predicates .push(parse_quote!(#ident #type_generics: ::core::fmt::Debug)); let (error_impl_generics, error_type_generics, error_where_clause) = error_generics.split_for_impl(); Ok(quote! { #(#identities)* impl #impl_generics ::kaptan_err_reportable::ReportableError for #ident #type_generics #where_clause { fn error_id(&self) -> ::kaptan_err_reportable::ErrorId { match self { #(#arms),* } } } impl #impl_generics ::core::fmt::Display for #ident #type_generics #where_clause { fn fmt( &self, formatter: &mut ::core::fmt::Formatter<'_>, ) -> ::core::fmt::Result { ::core::fmt::Display::fmt( &::error_id(self).get(), formatter, ) } } impl #error_impl_generics ::core::error::Error for #ident #error_type_generics #error_where_clause {} }) } fn variant_id(variant: &Variant) -> syn::Result { let mut id = None; for attribute in &variant.attrs { if !attribute.path().is_ident("kaptan_error") { continue; } attribute.parse_nested_meta(|meta| { if !meta.path.is_ident("id") { return Err(meta.error("unsupported Kaptan error property; expected `id`")); } if id.is_some() { return Err(meta.error("duplicate Kaptan error `id`")); } let literal: LitInt = meta.value()?.parse()?; let value = literal.base10_parse::()?; if value == 0 { return Err(Error::new( literal.span(), "Kaptan error ID zero is reserved", )); } id = Some(value); Ok(()) })?; } id.ok_or_else(|| { Error::new_spanned( &variant.ident, "missing `#[kaptan_error(id = ...)]` on Kaptan error variant", ) }) }