Skip to main content

anodized_core/instrument/traits/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use quote::quote;
5use syn::{
6    Attribute, Block, FnArg, ImplItem, ImplItemFn, Pat, TraitItem, TraitItemFn, Visibility,
7    parse_quote,
8};
9
10use crate::{
11    DataSpec, Spec,
12    instrument::{Config, find_spec_attr, make_item_error},
13};
14
15impl Config {
16    /// Expand trait items by mangling each method and adding a wrapper default impl.
17    ///
18    /// Mangling a function involves the following:
19    /// 1. Rename the function following the pattern: `fn add` -> `fn __anodized_add`.
20    /// 2. Make a new function with the original name that has a default impl; the
21    ///    default impl performs runtime validation and calls the mangled function.
22    pub fn instrument_trait(
23        &self,
24        spec: DataSpec,
25        mut the_trait: syn::ItemTrait,
26    ) -> syn::Result<syn::ItemTrait> {
27        // Currently we don't support any spec arguments for traits themselves.
28        if !spec.is_empty() {
29            return Err(spec.spec_err(
30                "Unsupported spec element on trait. Try placing it on an item inside the trait",
31            ));
32        }
33        let _ = move || spec;
34
35        let mut new_trait_items = Vec::with_capacity(the_trait.items.len() * 5);
36
37        for item in the_trait.items.into_iter() {
38            match item {
39                TraitItem::Fn(mut func) => {
40                    let (spec_attr, other_attrs) = find_spec_attr(func.attrs)?;
41                    // NOTE: We have no way of knowing which attributes are
42                    //   "external" - meant for the interface and belong on the wrapper,
43                    //   "internal" - meant for the mangled implementation.
44                    //   Right now we put all attribs on both functions, but that's certainly
45                    //   not going to work in every situation.
46                    func.attrs = other_attrs.clone();
47
48                    let fn_spec: Spec = match spec_attr {
49                        Some(spec_attr) => spec_attr.parse_args()?,
50                        None => Spec::empty(),
51                    };
52
53                    if self.embed_spec {
54                        let attrs: [Attribute; 2] = [
55                            parse_quote!(#[doc(hidden)]),
56                            parse_quote!(#[allow(warnings)]),
57                        ];
58
59                        // Embed `spec` elements as `__anodized_fn_*` items.
60                        let spec_trait_qualifiers_const = Self::build_qualifier_const_item(
61                            &attrs,
62                            "__anodized_fn_qualifiers_trait",
63                            fn_spec.qualifiers,
64                            &func.sig.ident,
65                        );
66                        let spec_qualifiers_const = Self::build_qualifier_const_item(
67                            &attrs,
68                            "__anodized_fn_qualifiers",
69                            fn_spec.qualifiers,
70                            &func.sig.ident,
71                        );
72                        let spec_requires_fn = TraitItemFn {
73                            attrs: attrs.to_vec(),
74                            sig: Self::build_spec_fn_sig("__anodized_fn_requires", &func.sig),
75                            default: Some(Self::build_precondition_fn_body(&fn_spec.requires)),
76                            semi_token: None,
77                        };
78                        let spec_maintains_fn = TraitItemFn {
79                            attrs: attrs.to_vec(),
80                            sig: Self::build_spec_fn_sig("__anodized_fn_maintains", &func.sig),
81                            default: Some(Self::build_precondition_fn_body(&fn_spec.maintains)),
82                            semi_token: None,
83                        };
84                        let spec_ensures_fn = TraitItemFn {
85                            attrs: attrs.to_vec(),
86                            sig: Self::build_spec_fn_sig("__anodized_fn_ensures", &func.sig),
87                            default: Some(Self::build_poscondition_fn_body(
88                                &fn_spec.captures,
89                                &fn_spec.ensures,
90                                &func.sig.output,
91                            )?),
92                            semi_token: None,
93                        };
94
95                        new_trait_items.push(TraitItem::Const(spec_trait_qualifiers_const));
96                        new_trait_items.push(TraitItem::Const(spec_qualifiers_const));
97                        new_trait_items.push(TraitItem::Fn(spec_requires_fn));
98                        new_trait_items.push(TraitItem::Fn(spec_maintains_fn));
99                        new_trait_items.push(TraitItem::Fn(spec_ensures_fn));
100                    }
101
102                    if self.emit_anything() {
103                        let mangled_ident = mangle_ident(&func.sig.ident);
104
105                        let mut mangled_fn = func.clone();
106                        mangled_fn.sig.ident = mangled_ident.clone();
107                        mangled_fn.attrs.retain(|attr| !attr.path().is_ident("doc"));
108                        mangled_fn.attrs.push(parse_quote!(#[doc(hidden)]));
109                        new_trait_items.push(TraitItem::Fn(mangled_fn));
110
111                        let call_args = build_call_args(&func.sig.inputs)?;
112                        let forwarding_body: Block = parse_quote!({
113                            Self::#mangled_ident(#(#call_args),*)
114                        });
115                        func.default = Some(forwarding_body);
116                        func.semi_token = None;
117                    }
118
119                    func.attrs = other_attrs;
120
121                    if let Some(default_body) = &mut func.default {
122                        // NOTE: Needed to handle loop specs in the body of the default impl.
123                        self.instrument_fn(&fn_spec, &func.sig, default_body)?;
124                    }
125                    new_trait_items.push(TraitItem::Fn(func));
126                }
127                TraitItem::Const(mut const_item) => {
128                    let (spec, attrs) = find_spec_attr(const_item.attrs)?;
129                    if let Some(ref spec_attr) = spec {
130                        return Err(make_item_error(&spec_attr, "trait const"));
131                    }
132                    const_item.attrs = attrs;
133                    new_trait_items.push(TraitItem::Const(const_item));
134                }
135                TraitItem::Type(mut type_item) => {
136                    let (spec, attrs) = find_spec_attr(type_item.attrs)?;
137                    if let Some(ref spec_attr) = spec {
138                        return Err(make_item_error(&spec_attr, "trait type"));
139                    }
140                    type_item.attrs = attrs;
141                    new_trait_items.push(TraitItem::Type(type_item));
142                }
143                TraitItem::Macro(mut macro_item) => {
144                    let (spec, attrs) = find_spec_attr(macro_item.attrs)?;
145                    if let Some(ref spec_attr) = spec {
146                        return Err(make_item_error(&spec_attr, "trait macro"));
147                    }
148                    macro_item.attrs = attrs;
149                    new_trait_items.push(TraitItem::Macro(macro_item));
150                }
151                TraitItem::Verbatim(token_stream) => {
152                    new_trait_items.push(TraitItem::Verbatim(token_stream));
153                }
154                _ => unimplemented!(),
155            }
156        }
157        the_trait.items = new_trait_items;
158        Ok(the_trait)
159    }
160
161    /// Expand impl items by mangling methods for trait impls.
162    ///
163    /// The `#[spec]` attribute on an impl `fn` must narrow the `#[spec]` of the trait `fn`:
164    /// - The impl's preconditions must follow from the trait's preconditions.
165    /// - The impl's postconditions must entail the trait's postconditions.
166    pub fn instrument_trait_impl(
167        &self,
168        spec: DataSpec,
169        mut the_impl: syn::ItemImpl,
170    ) -> syn::Result<syn::ItemImpl> {
171        let Some((trait_bang, ref trait_path, _trait_for)) = the_impl.trait_ else {
172            return Err(make_item_error(&the_impl, "inherent impl"));
173        };
174
175        if trait_bang.is_some() {
176            return Err(make_item_error(&the_impl, "negative trait impl"));
177        }
178
179        if !spec.is_empty() {
180            return Err(spec.spec_err("Unsupported spec element on trait impl."));
181        }
182
183        let mut new_items = Vec::with_capacity(the_impl.items.len() * 4);
184
185        for item in the_impl.items.into_iter() {
186            match item {
187                ImplItem::Fn(mut func) => {
188                    let (spec_attr, func_attrs) = find_spec_attr(func.attrs)?;
189                    func.attrs = func_attrs;
190
191                    if func.sig.ident.to_string().starts_with("__anodized_") {
192                        return Err(syn::Error::new_spanned(
193                            func.sig.ident,
194                            r#"An item with the `__anodized_` prefix is internal. Do not implement it directly.
195Instead, ensure that both the trait and the impl fn have a `#[spec]` annotation."#,
196                        ));
197                    }
198
199                    let fn_spec: Spec = match spec_attr {
200                        Some(spec_attr) => spec_attr.parse_args()?,
201                        None => Spec::empty(),
202                    };
203
204                    if self.embed_spec {
205                        let attrs: [Attribute; 2] = [
206                            parse_quote!(#[doc(hidden)]),
207                            parse_quote!(#[allow(warnings)]),
208                        ];
209
210                        // Embed `spec` elements as `__anodized_fn_*` items.
211                        let spec_qualifiers_const = Self::build_qualifier_const_item(
212                            &attrs,
213                            "__anodized_fn_qualifiers",
214                            fn_spec.qualifiers,
215                            &func.sig.ident,
216                        );
217                        let spec_requires_fn = ImplItemFn {
218                            attrs: attrs.to_vec(),
219                            sig: Self::build_spec_fn_sig("__anodized_fn_requires", &func.sig),
220                            block: Self::build_precondition_fn_body(&fn_spec.requires),
221                            vis: Visibility::Inherited,
222                            defaultness: None,
223                        };
224                        let spec_maintains_fn = ImplItemFn {
225                            attrs: attrs.to_vec(),
226                            sig: Self::build_spec_fn_sig("__anodized_fn_maintains", &func.sig),
227                            block: Self::build_precondition_fn_body(&fn_spec.maintains),
228                            vis: Visibility::Inherited,
229                            defaultness: None,
230                        };
231                        let spec_ensures_fn = ImplItemFn {
232                            attrs: attrs.to_vec(),
233                            sig: Self::build_spec_fn_sig("__anodized_fn_ensures", &func.sig),
234                            block: Self::build_poscondition_fn_body(
235                                &fn_spec.captures,
236                                &fn_spec.ensures,
237                                &func.sig.output,
238                            )?,
239                            vis: Visibility::Inherited,
240                            defaultness: None,
241                        };
242
243                        new_items.push(ImplItem::Const(spec_qualifiers_const));
244                        new_items.push(ImplItem::Fn(spec_requires_fn));
245                        new_items.push(ImplItem::Fn(spec_maintains_fn));
246                        new_items.push(ImplItem::Fn(spec_ensures_fn));
247                    }
248
249                    self.instrument_fn(&fn_spec, &func.sig, &mut func.block)?;
250
251                    if self.embed_spec {
252                        // Add a compile-time check to the body.
253                        func.block.stmts.insert(
254                            0,
255                            Self::build_qualifier_check_stmt(
256                                &func.sig.ident,
257                                &the_impl.self_ty,
258                                trait_path,
259                            ),
260                        );
261                    }
262
263                    if self.emit_anything() {
264                        func.sig.ident = mangle_ident(&func.sig.ident);
265
266                        // Add a default `#[inline]` attribute unless one is already there.
267                        // The caller can supress this with `#[inline(never)]`
268                        if !has_inline_attr(&func.attrs) {
269                            func.attrs.push(parse_quote!(#[inline]));
270                        }
271                    }
272                    new_items.push(ImplItem::Fn(func));
273                }
274                ImplItem::Const(mut const_item) => {
275                    let (spec, attrs) = find_spec_attr(const_item.attrs)?;
276                    if let Some(ref spec_attr) = spec {
277                        return Err(make_item_error(&spec_attr, "trait impl const"));
278                    }
279                    const_item.attrs = attrs;
280                    new_items.push(ImplItem::Const(const_item));
281                }
282                ImplItem::Type(mut type_item) => {
283                    let (spec, attrs) = find_spec_attr(type_item.attrs)?;
284                    if let Some(ref spec_attr) = spec {
285                        return Err(make_item_error(&spec_attr, "trait impl type"));
286                    }
287                    type_item.attrs = attrs;
288                    new_items.push(ImplItem::Type(type_item));
289                }
290                ImplItem::Macro(mut macro_item) => {
291                    let (spec, attrs) = find_spec_attr(macro_item.attrs)?;
292                    if let Some(ref spec_attr) = spec {
293                        return Err(make_item_error(&spec_attr, "trait impl macro"));
294                    }
295                    macro_item.attrs = attrs;
296                    new_items.push(ImplItem::Macro(macro_item));
297                }
298                ImplItem::Verbatim(token_stream) => {
299                    new_items.push(ImplItem::Verbatim(token_stream))
300                }
301                _ => unimplemented!(),
302            };
303        }
304
305        the_impl.items = new_items;
306        Ok(the_impl)
307    }
308}
309
310/// Build argument tokens for calling the mangled trait method from the wrapper.
311///
312/// Purpose: the wrapper method needs to forward its arguments to the mangled
313/// implementation, so this extracts a usable token for each input.
314///
315/// Examples (inputs -> output tokens):
316/// - `fn f(&self, x: i32)` -> `self, x`
317/// - `fn f(self, a: u8, b: u8)` -> `self, a, b`
318///
319/// The caller is responsible for ensuring these tokens are used in a call
320/// expression like `Self::__anodized_f(#(#args),*)`.
321///
322/// Callers: only `instrument_trait` in this module should use this; it is not
323/// part of the public API.
324fn build_call_args(
325    inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
326) -> syn::Result<Vec<proc_macro2::TokenStream>> {
327    let mut args = Vec::new();
328    for input in inputs.iter() {
329        match input {
330            FnArg::Receiver(_) => {
331                args.push(quote! { self });
332            }
333            FnArg::Typed(pat) => match pat.pat.as_ref() {
334                Pat::Ident(pat_ident) => {
335                    let ident = &pat_ident.ident;
336                    args.push(quote! { #ident });
337                }
338                _ => {
339                    return Err(syn::Error::new_spanned(
340                        &pat.pat,
341                        "unsupported pattern in trait method arguments",
342                    ));
343                }
344            },
345        }
346    }
347    Ok(args)
348}
349
350/// Prefix an identifier with `__anodized_`, preserving the original span.
351/// Used when generating mangled method names in trait and impl expansion.
352fn mangle_ident(original_ident: &syn::Ident) -> syn::Ident {
353    syn::Ident::new(
354        &format!("__anodized_{original_ident}"),
355        original_ident.span(),
356    )
357}
358
359/// Checks to see if any `#[inline]` (with or without arg) exists in the function's attribs.
360fn has_inline_attr(attrs: &[syn::Attribute]) -> bool {
361    attrs.iter().any(|attr| attr.path().is_ident("inline"))
362}