Skip to main content

anodized_core/
instrument.rs

1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::{
4    Attribute, Block, FnArg, Ident, ItemConst, ItemFn, ItemImpl, ItemTrait, Result, ReturnType,
5    Signature, parse_quote,
6};
7
8use crate::{DataSpec, Spec};
9
10pub mod data;
11pub mod fns;
12pub mod impls;
13pub mod loops;
14pub mod traits;
15
16#[derive(Debug, Clone)]
17pub enum Mode {
18    /// Make no changes to the code.
19    ChangeNothing,
20    /// Inject code to enable compile-time and/or runtime checks.
21    InjectChecks(CheckSettings),
22    /// Embed spec elements as new items without changing existing code.
23    EmbedSpecs,
24}
25
26#[derive(Debug, Clone)]
27pub struct CheckSettings {
28    /// Print errors about violated clauses.
29    pub does_print: bool,
30    /// Panic on a violated pre/postcondition or invariant.
31    pub does_panic: Option<PanicSettings>,
32}
33
34#[derive(Debug, Clone)]
35pub struct PanicSettings {
36    /// Generate an entry point that defers the panic. Used for fuzzing, PBT, etc.
37    pub has_try_fn: bool,
38}
39
40impl Mode {
41    pub fn changes_anything(&self) -> bool {
42        !matches!(self, Mode::ChangeNothing)
43    }
44
45    pub fn emits_try_fn(&self) -> bool {
46        if let Self::InjectChecks(check_settings) = self
47            && let Some(panic_settings) = &check_settings.does_panic
48        {
49            panic_settings.has_try_fn
50        } else {
51            false
52        }
53    }
54
55    pub fn with_try_fn(&self, value: bool) -> Self {
56        match self {
57            Mode::ChangeNothing => Mode::ChangeNothing,
58            Mode::InjectChecks(check_settings) => {
59                let mut check_settings = check_settings.clone();
60                if let Some(panic_settings) = &mut check_settings.does_panic {
61                    panic_settings.has_try_fn = value;
62                };
63                Mode::InjectChecks(check_settings)
64            }
65            Mode::EmbedSpecs => Mode::EmbedSpecs,
66        }
67    }
68
69    pub fn instrument_item_fn(&self, spec: Spec, mut item_fn: ItemFn) -> Result<TokenStream> {
70        let mut tokens = TokenStream::new();
71
72        if item_fn.sig.ident.to_string().starts_with("__anodized_") {
73            return Err(syn::Error::new_spanned(
74                item_fn.sig.ident,
75                r#"An item with the `__anodized_` prefix is internal. Do not implement it directly.
76Instead, you likely need to place a `#[spec]` attribute on an enclosing trait or impl block."#,
77            ));
78        }
79
80        if let Self::EmbedSpecs = self {
81            // Embed `spec` elements as `__anodized_fn_*` items.
82            let attrs: [Attribute; 2] = [
83                parse_quote!(#[doc(hidden)]),
84                parse_quote!(#[allow(warnings)]),
85            ];
86
87            let spec_qualifiers_const: ItemConst = Self::build_qualifier_const_item(
88                &attrs,
89                "__anodized_fn_qualifiers",
90                spec.qualifiers,
91                &item_fn.sig.ident,
92            );
93            let spec_requires_fn = ItemFn {
94                attrs: attrs.to_vec(),
95                vis: syn::Visibility::Inherited,
96                sig: Self::build_precondition_fn_sig("__anodized_fn_requires", &item_fn.sig),
97                block: Box::new(Self::build_precondition_fn_body(
98                    &spec.requires,
99                    &spec.maintains,
100                )),
101            };
102            let spec_ensures_fn = ItemFn {
103                attrs: attrs.to_vec(),
104                vis: syn::Visibility::Inherited,
105                sig: Self::build_postcondition_fn_sig("__anodized_fn_ensures", &item_fn.sig),
106                block: Box::new(Self::build_postcondition_fn_body(
107                    &spec.maintains,
108                    &spec.captures,
109                    &spec.ensures,
110                )?),
111            };
112
113            spec_qualifiers_const.to_tokens(&mut tokens);
114            spec_requires_fn.to_tokens(&mut tokens);
115            spec_ensures_fn.to_tokens(&mut tokens);
116        }
117
118        // Instrument function body.
119        self.instrument_fn(&spec, &item_fn.sig, &mut item_fn.block)?;
120
121        if let Self::InjectChecks(check_settings) = self
122            && let Some(ref panic_settings) = check_settings.does_panic
123            && panic_settings.has_try_fn
124        {
125            // Build a wrapper that forwards to the "try_fn" entry point.
126            let mut wrapper_fn = item_fn.clone();
127            let mangled_ident =
128                Self::build_try_fn_wrapper(false, &mut wrapper_fn.sig, wrapper_fn.block.as_mut());
129            wrapper_fn.to_tokens(&mut tokens);
130
131            // Create the "try_fn" entry point for e.g. fuzzing and PBT.
132            item_fn.sig.ident = mangled_ident;
133            item_fn.sig.output = match item_fn.sig.output {
134                ReturnType::Default => parse_quote!(-> ::anodized::result::Result<()>),
135                ReturnType::Type(ra, ty) => {
136                    parse_quote!(#ra ::anodized::result::Result<#ty>)
137                }
138            };
139            item_fn.attrs = vec![parse_quote!(#[doc(hidden)]), parse_quote!(#[inline])];
140        }
141
142        item_fn.to_tokens(&mut tokens);
143        Ok(tokens)
144    }
145
146    fn build_try_fn_wrapper(is_impl: bool, sig: &mut Signature, body: &mut Block) -> Ident {
147        let mangled_ident = fns::make_try_fn_ident(&sig.ident);
148
149        Self::build_wrapper_fn_signature(sig);
150
151        let args = sig.inputs.iter().map(|arg| match arg {
152            FnArg::Receiver(receiver) => receiver.self_token.to_token_stream(),
153            FnArg::Typed(pat_type) => pat_type.pat.to_token_stream(),
154        });
155
156        let maybe_self = match is_impl {
157            true => quote!(Self::),
158            false => quote!(),
159        };
160
161        let maybe_await = match &sig.asyncness {
162            Some(_) => quote!(.await),
163            None => quote!(),
164        };
165
166        *body = parse_quote! {
167            {
168                match #maybe_self #mangled_ident(#(#args),*) #maybe_await {
169                    ::anodized::result::Result::Ok(output) => output,
170                    ::anodized::result::Result::Err(
171                        ::anodized::result::Error::Pre(errors)
172                    ) => panic!("precondition failed:{errors}"),
173                    ::anodized::result::Result::Err(
174                        ::anodized::result::Error::Post(_, errors)
175                    ) => panic!("postcondition failed:{errors}"),
176                }
177            }
178        };
179
180        mangled_ident
181    }
182
183    fn build_wrapper_fn_signature(sig: &mut Signature) {
184        use syn::spanned::Spanned;
185        for (i, arg) in sig.inputs.iter_mut().enumerate() {
186            match arg {
187                FnArg::Receiver(_) => {}
188                FnArg::Typed(pat_type) => {
189                    let name = Ident::new(&format!("input_{i}"), pat_type.span());
190                    pat_type.pat = parse_quote!(#name);
191                }
192            }
193        }
194    }
195
196    pub fn instrument_item_impl(&self, spec: DataSpec, item_impl: ItemImpl) -> Result<TokenStream> {
197        let new_impl = self.instrument_impl(spec, item_impl)?;
198        Ok(new_impl.to_token_stream())
199    }
200
201    pub fn instrument_item_trait(
202        &self,
203        spec: DataSpec,
204        item_trait: ItemTrait,
205    ) -> Result<TokenStream> {
206        let new_trait = self.instrument_trait(spec, item_trait)?;
207        Ok(new_trait.to_token_stream())
208    }
209
210    pub fn instrument_item_trait_impl(
211        &self,
212        spec: DataSpec,
213        item_impl: ItemImpl,
214    ) -> Result<TokenStream> {
215        let new_trait_impl = self.instrument_trait_impl(spec, item_impl)?;
216        Ok(new_trait_impl.to_token_stream())
217    }
218}
219
220#[cfg(test)]
221impl Mode {
222    pub(crate) const DEFAULT: Self = Mode::InjectChecks(CheckSettings::DEFAULT);
223}
224
225#[cfg(test)]
226impl CheckSettings {
227    pub(crate) const DEFAULT: Self = Self {
228        does_print: false,
229        does_panic: None,
230    };
231
232    pub(crate) const PRINT: Self = Self {
233        does_print: true,
234        does_panic: None,
235    };
236
237    pub(crate) const PRINT_AND_PANIC: Self = Self {
238        does_print: true,
239        does_panic: Some(PanicSettings { has_try_fn: false }),
240    };
241
242    pub(crate) const PRINT_AND_TRY: Self = Self {
243        does_print: true,
244        does_panic: Some(PanicSettings { has_try_fn: true }),
245    };
246}
247
248/// Make an error message to say that some item is unsupported.
249pub fn make_item_error<T: ToTokens>(tokens: &T, item_descr: &str) -> syn::Error {
250    let msg = format!(
251        r#"The #[spec] attribute doesn't yet support this item: {}.
252If this is a problem for your use case, please open a feature
253request at https://github.com/anodized-rs/anodized/issues/new"#,
254        item_descr
255    );
256    syn::Error::new_spanned(tokens, msg)
257}
258
259/// Finds the `[spec]` attrib in an attribute list.
260///
261/// Returns the spec [Attribute] and the remaining attributes.
262fn find_spec_attr(attrs: Vec<Attribute>) -> syn::Result<(Option<Attribute>, Vec<Attribute>)> {
263    let mut spec_attr = None;
264    let mut other_attrs = Vec::new();
265
266    for attr in attrs {
267        if attr.path().is_ident("spec") {
268            if spec_attr.is_some() {
269                return Err(syn::Error::new_spanned(
270                    attr,
271                    "multiple `#[spec]` attributes on a single item are not supported",
272                ));
273            }
274            spec_attr = Some(attr);
275        } else {
276            other_attrs.push(attr);
277        }
278    }
279
280    Ok((spec_attr, other_attrs))
281}