diff --git a/crates/anodized-core/src/annotate/mod.rs b/crates/anodized-core/src/annotate/mod.rs index c8b8b5e..afafd16 100644 --- a/crates/anodized-core/src/annotate/mod.rs +++ b/crates/anodized-core/src/annotate/mod.rs @@ -106,6 +106,7 @@ impl Parse for Spec { maintains, captures, ensures, + span: input.span(), }) } } diff --git a/crates/anodized-core/src/annotate/tests.rs b/crates/anodized-core/src/annotate/tests.rs index 7cb15cf..2fdafc9 100644 --- a/crates/anodized-core/src/annotate/tests.rs +++ b/crates/anodized-core/src/annotate/tests.rs @@ -1,6 +1,7 @@ use crate::test_util::assert_spec_eq; use super::*; +use proc_macro2::Span; use syn::parse_quote; #[test] @@ -18,6 +19,7 @@ fn simple_spec() { closure: parse_quote! { |output| output > x }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -40,6 +42,7 @@ fn all_clauses() { closure: parse_quote! { |z| z >= x }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -101,6 +104,7 @@ fn array_of_conditions() { cfg: None, }, ], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -120,6 +124,7 @@ fn ensures_with_explicit_closure() { closure: parse_quote! { |result| result.is_ok() || result.unwrap_err().kind() == ErrorKind::NotFound }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -151,6 +156,7 @@ fn multiple_clauses_of_same_flavor() { cfg: None, }, ], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -193,6 +199,7 @@ fn mixed_single_and_array_clauses() { cfg: None, }, ], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -218,6 +225,7 @@ fn cfg_attributes() { closure: parse_quote! { |output| output < x }, cfg: Some(parse_quote! { not(debug_assertions) }), }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -269,6 +277,7 @@ fn macro_in_condition() { closure: parse_quote! { |output| matches!(self.state, State::Running) }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -298,6 +307,7 @@ fn binds_pattern() { cfg: None, }, ], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -323,6 +333,7 @@ fn multiple_conditions() { maintains: vec![parse_quote! { self.items.len() <= self.items.capacity() }], captures: vec![], ensures: vec![], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -352,6 +363,7 @@ fn rename_return_value() { cfg: None, }, ], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -375,6 +387,7 @@ fn captures_simple_identifier() { closure: parse_quote! { |output| output == old_count + 1 }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -398,6 +411,7 @@ fn captures_identifier_with_alias() { closure: parse_quote! { |output| output > prev_value }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -449,6 +463,7 @@ fn captures_array() { cfg: None, }, ], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -475,6 +490,7 @@ fn captures_with_all_clauses() { closure: parse_quote! { |result| result > old_val }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -507,6 +523,7 @@ fn captures_array_expression() { closure: parse_quote! { |output| slice.len() == 3 }, cfg: None, }], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -576,6 +593,7 @@ fn captures_edge_case_cast_expr() { alias: parse_quote! { old_red }, }], ensures: vec![], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -605,6 +623,7 @@ fn captures_edge_case_array_of_cast_exprs() { alias: parse_quote! { r8g8b8 }, }], ensures: vec![], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); @@ -638,6 +657,7 @@ fn captures_edge_case_list_of_cast_exprs() { }, ], ensures: vec![], + span: Span::call_site(), }; assert_spec_eq(&spec, &expected); diff --git a/crates/anodized-core/src/instrument/function/mod.rs b/crates/anodized-core/src/instrument/function/mod.rs index 4cab509..b1a6e98 100644 --- a/crates/anodized-core/src/instrument/function/mod.rs +++ b/crates/anodized-core/src/instrument/function/mod.rs @@ -8,7 +8,7 @@ use quote::{ToTokens, quote}; use syn::{Block, Ident, ItemFn, parse::Result, parse_quote}; impl Backend { - pub fn instrument_fn(self, spec: Spec, mut func: ItemFn) -> syn::Result { + pub fn instrument_fn(&self, spec: Spec, mut func: ItemFn) -> syn::Result { let is_async = func.sig.asyncness.is_some(); // Extract the return type from the function signature @@ -27,7 +27,7 @@ impl Backend { } fn instrument_fn_body( - self, + &self, spec: &Spec, original_body: &Block, is_async: bool, diff --git a/crates/anodized-core/src/instrument/mod.rs b/crates/anodized-core/src/instrument/mod.rs index 7bc7483..a187b01 100644 --- a/crates/anodized-core/src/instrument/mod.rs +++ b/crates/anodized-core/src/instrument/mod.rs @@ -1,7 +1,9 @@ use proc_macro2::TokenStream; use quote::quote; use syn::Meta; + pub mod function; +pub mod trait_spec; pub struct Backend { pub build_check: fn(Option<&Meta>, &TokenStream, &str, &TokenStream) -> TokenStream, diff --git a/crates/anodized-core/src/instrument/trait_spec/mod.rs b/crates/anodized-core/src/instrument/trait_spec/mod.rs new file mode 100644 index 0000000..51c69a2 --- /dev/null +++ b/crates/anodized-core/src/instrument/trait_spec/mod.rs @@ -0,0 +1,218 @@ +use quote::quote; +use syn::{Attribute, FnArg, ImplItem, ItemFn, Pat, TraitItem, parse_quote}; + +use crate::{Spec, instrument::Backend}; + +impl Backend { + /// Expand trait items by mangling each method and adding a wrapper default impl. + /// + /// Mangling a function involves the following: + /// 1. Rename the function following the pattern: `fn add` -> `fn __anodized_add` + /// 2. Make a new function with the original name that has a default impl; the + /// default impl performs runtime validation and calls the mangled function. + pub fn instrument_trait( + &self, + spec: Spec, + mut the_trait: syn::ItemTrait, + ) -> syn::Result { + // Currently we don't support any spec arguments for traits themselves. + if !spec.is_empty() { + return Err(spec.spec_err("unsupported spec element on trait. Maybe it should go on an item within the trait")); + } + + let mut new_trait_items = Vec::with_capacity(the_trait.items.len() * 2); + + for item in the_trait.items.into_iter() { + match item { + TraitItem::Fn(mut func) => { + let (spec, other_attrs) = parse_spec_attr(func.attrs)?; + + //ISSUE: We have no way of knowing which attributes are "externally facing", i.e. they are meant + // for the interface and therefore belong on the wrapper with the un-mangled name, and which ones + // are "internally facing", and are meant for the mangled implementation. Right now we put all + // attribs on both functions, but that's certainly not going to work in every situation + func.attrs = other_attrs.clone(); + + let original_ident = func.sig.ident.clone(); + let mangled_ident = mangle_ident(&original_ident); + + let mut mangled_fn = func.clone(); + mangled_fn.sig.ident = mangled_ident.clone(); + mangled_fn.attrs.retain(|attr| !attr.path().is_ident("doc")); + mangled_fn.attrs.push(parse_quote!(#[doc(hidden)])); + + let call_args = build_call_args(&func.sig.inputs)?; + let mut wrapper_block: syn::Block = parse_quote!({ + Self::#mangled_ident(#(#call_args),*) + }); + + if let Some((spec, _spec_attr)) = spec { + let wrapper_item = ItemFn { + attrs: Vec::new(), + vis: syn::Visibility::Inherited, + sig: func.sig.clone(), + block: Box::new(wrapper_block), + }; + let instrumented = self.instrument_fn(spec, wrapper_item)?; + wrapper_block = *instrumented.block; + } + + let mut wrapper_fn = func; + wrapper_fn.attrs = other_attrs; + wrapper_fn.default = Some(wrapper_block); + wrapper_fn.semi_token = None; + + new_trait_items.push(TraitItem::Fn(mangled_fn)); + new_trait_items.push(TraitItem::Fn(wrapper_fn)); + } + other => new_trait_items.push(other), + } + } + the_trait.items = new_trait_items; + Ok(the_trait) + } + + /// Expand impl items by mangling methods for trait impls + /// + /// `#[spec]` attributes on the impl items themselves may not have `requires`, `maintains`, nor `ensures` directives. + pub fn instrument_trait_impl( + &self, + spec: Spec, + mut the_impl: syn::ItemImpl, + ) -> syn::Result { + if !spec.is_empty() { + return Err(spec.spec_err("unsupported spec element on impl block. Maybe it should go on an item within the block")); + } + + if the_impl.trait_.is_none() { + return Err(syn::Error::new_spanned( + &the_impl.self_ty, + "anodized only supports specs on trait impl blocks", + )); + } + + let mut new_items = Vec::with_capacity(the_impl.items.len()); + + for item in the_impl.items.into_iter() { + match item { + ImplItem::Fn(mut func) => { + let (spec, mut func_attrs) = parse_spec_attr(func.attrs)?; + if let Some((_, spec_attr)) = spec { + return Err(syn::Error::new_spanned( + spec_attr, + "trait impl methods may not have spec attributes. Implementations must respect the contract of the trait interface. Please file an issue on github if you need implementation-specific validation", + )); + + // QUESTION: Do we want to allow a spec, so long as it doesn't contain `requires`, `maintains`, nor `ensures`? + // + // if !spec.requires.is_empty() || !spec.maintains.is_empty() || !spec.ensures.is_empty() { + // return Err(syn::Error::new_spanned( + // spec_attr, + // "trait impl method specs may not contain `requires`, `maintains`, nor `ensures`", + // )); + // } + // func_attrs.push(spec_attr); + } + + let original_ident = func.sig.ident.clone(); + if !original_ident.to_string().starts_with("__anodized_") { + func.sig.ident = mangle_ident(&original_ident); + } + + //Add a default `#[inline]` attribute unless one is already there. + //The caller can supress this with `#[inline(never)]` + if !has_inline_attr(&func_attrs) { + func_attrs.push(parse_quote!(#[inline])); + } + + func.attrs = func_attrs; + new_items.push(ImplItem::Fn(func)); + } + other => new_items.push(other), + } + } + + the_impl.items = new_items; + Ok(the_impl) + } +} + +/// Build argument tokens for calling the mangled trait method from the wrapper. +/// +/// Purpose: the wrapper method needs to forward its arguments to the mangled +/// implementation, so this extracts a usable token for each input. +/// +/// Examples (inputs -> output tokens): +/// - `fn f(&self, x: i32)` -> `self, x` +/// - `fn f(self, a: u8, b: u8)` -> `self, a, b` +/// +/// The caller is responsible for ensuring these tokens are used in a call +/// expression like `Self::__anodized_f(#(#args),*)`. +/// +/// Callers: only `instrument_trait` in this module should use this; it is not +/// part of the public API. +fn build_call_args( + inputs: &syn::punctuated::Punctuated, +) -> syn::Result> { + let mut args = Vec::new(); + for input in inputs.iter() { + match input { + FnArg::Receiver(_) => { + args.push(quote! { self }); + } + FnArg::Typed(pat) => match pat.pat.as_ref() { + Pat::Ident(pat_ident) => { + let ident = &pat_ident.ident; + args.push(quote! { #ident }); + } + _ => { + return Err(syn::Error::new_spanned( + &pat.pat, + "unsupported pattern in trait method arguments", + )); + } + }, + } + } + Ok(args) +} + +/// Prefix an identifier with `__anodized_`, preserving the original span. +/// Used when generating mangled method names in trait and impl expansion. +fn mangle_ident(original_ident: &syn::Ident) -> syn::Ident { + syn::Ident::new( + &format!("__anodized_{original_ident}"), + original_ident.span(), + ) +} + +/// Checks to see if any `#[inline]` (with or without arg) exists in the function's attribs +fn has_inline_attr(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| attr.path().is_ident("inline")) +} + +/// Parses out the `[spec]` attrib from a function's attribute list +/// +/// Returns the parsed spec, the spec [Attribute] and the remaining attributes +fn parse_spec_attr( + attrs: Vec, +) -> syn::Result<(Option<(Spec, Attribute)>, Vec)> { + let mut spec = None; + let mut other_attrs = Vec::new(); + + for attr in attrs { + if attr.path().is_ident("spec") { + if spec.is_some() { + return Err(syn::Error::new_spanned( + attr, + "multiple `#[spec]` attributes on a single method are not supported", + )); + } + spec = Some((attr.parse_args::()?, attr)); + } else { + other_attrs.push(attr); + } + } + + Ok((spec, other_attrs)) +} diff --git a/crates/anodized-core/src/lib.rs b/crates/anodized-core/src/lib.rs index c0899bb..17ed27a 100644 --- a/crates/anodized-core/src/lib.rs +++ b/crates/anodized-core/src/lib.rs @@ -1,5 +1,6 @@ #![doc = include_str!("../README.md")] +use proc_macro2::Span; use syn::{Expr, Ident, Meta}; pub mod annotate; @@ -19,6 +20,22 @@ pub struct Spec { pub captures: Vec, /// Postconditions: conditions that must hold when the function returns. pub ensures: Vec, + /// The span in the source code, from which this spec was parsed + span: Span, +} + +impl Spec { + /// Returns `true` if the spec contract is empty (specifies nothing), otherwise returns `false` + pub fn is_empty(&self) -> bool { + self.requires.is_empty() + && self.maintains.is_empty() + && self.ensures.is_empty() + && self.captures.is_empty() + } + /// Call to construct an error from the whole spec + pub fn spec_err(&self, message: &str) -> syn::Error { + syn::Error::new::<&str>(self.span, message) + } } /// A condition represented by a `bool`-valued expression. diff --git a/crates/anodized-core/src/test_util.rs b/crates/anodized-core/src/test_util.rs index 281fa7a..7f0caab 100644 --- a/crates/anodized-core/src/test_util.rs +++ b/crates/anodized-core/src/test_util.rs @@ -24,6 +24,7 @@ pub fn assert_spec_eq(left: &Spec, right: &Spec) { maintains: left_maintains, captures: left_captures, ensures: left_ensures, + span: _, } = left; let Spec { @@ -31,6 +32,7 @@ pub fn assert_spec_eq(left: &Spec, right: &Spec) { maintains: right_maintains, captures: right_captures, ensures: right_ensures, + span: _, } = right; assert_slice_eq( diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index 8063b5d..e75645a 100644 --- a/crates/anodized/Cargo.toml +++ b/crates/anodized/Cargo.toml @@ -14,6 +14,7 @@ license.workspace = true proc-macro = true [features] +# default = ["runtime-check-and-print"] runtime-check-and-panic = [] runtime-check-and-print = [] runtime-no-check = [] diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index b84fee5..c0c6372 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -42,7 +42,21 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { let result = match item { Item::Fn(func) => { let spec = parse_macro_input!(args as Spec); - BACKEND.instrument_fn(spec, func) + BACKEND + .instrument_fn(spec, func) + .map(|tokens| tokens.into_token_stream()) + } + Item::Trait(the_trait) => { + let spec = parse_macro_input!(args as Spec); + BACKEND + .instrument_trait(spec, the_trait) + .map(|tokens| tokens.into_token_stream()) + } + Item::Impl(the_impl) if the_impl.trait_.is_some() => { + let spec = parse_macro_input!(args as Spec); + BACKEND + .instrument_trait_impl(spec, the_impl) + .map(|tokens| tokens.into_token_stream()) } unsupported_item => { let item_type = item_to_string(&unsupported_item); @@ -57,7 +71,7 @@ request at https://github.com/mkovaxx/anodized/issues/new"#, }; match result { - Ok(item) => item.into_token_stream().into(), + Ok(item) => item.into(), Err(e) => e.to_compile_error().into(), } } diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs new file mode 100644 index 0000000..f867e4a --- /dev/null +++ b/crates/anodized/tests/basic_traits.rs @@ -0,0 +1,56 @@ +use anodized::spec; + +#[spec] +pub trait TestTrait { + /// Returns a current value + fn current(&self) -> u32; + + /// Does something + #[spec( + requires: x > 0, + captures: self.current() as old_val, + ensures: *output > old_val, + )] + fn do_something(&self, x: u32) -> u32 { + x * 2 + } +} + +struct TestStruct(u32); + +#[spec] +impl TestTrait for TestStruct { + fn current(&self) -> u32 { + self.0 + } + //GOAT, specs are no longer allowed on trait impls + // #[spec{ + // //gOAT, spec is allowed, but it must not have a `requires`, `maintains`, nor `ensures` directive + // // maintains: self.0 == 3, + // // ensures: *output > self.0, + // }] + #[inline(never)] + fn do_something(&self, x: u32) -> u32 { + x * self.0 + } +} + +struct TestStructConst; + +#[spec] +impl TestTrait for TestStructConst { + fn current(&self) -> u32 { + 0 + } +} + +#[test] +fn basic_trait_test() { + // Tests an impl of a trait with a spec, where there is a spec on both the implementation and on the trait interface + let test = TestStruct(3); + assert_eq!(test.do_something(500), 1500); + + // Tests a default method implementation coming from a trait + let test = TestStructConst; + assert_eq!(test.do_something(500), 1000); +}