From ad11629afd542db62546bae11d4f7a5fcb9fe242 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 9 Jan 2026 06:02:11 -0700 Subject: [PATCH 01/20] Starting towards parsing the spec macro on traits and trait items --- crates/anodized/src/lib.rs | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index b84fee5..f4376d2 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::ToTokens; -use syn::{Item, parse_macro_input}; +use syn::{Item, TraitItem, parse_macro_input}; use anodized_core::{Spec, instrument::Backend}; @@ -42,8 +42,40 @@ 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) => { + //Currently we don't support any markup for traits themselves - only the + // items within the trait + let _spec = parse_macro_input!(args as Spec); + + for item in &the_trait.items { + match item { + TraitItem::Fn(func) => { + let mut replacement_func = func.clone(); + let mut spec = None; + let mut other_attrs = Vec::new(); + for attr in &func.attrs { + if attr.path().is_ident("spec") { + match attr.parse_args::() { + Ok(parsed) => { + spec = Some(parsed) + }, + Err(e) => return e.to_compile_error().into() + } + } else { + other_attrs.push(attr.clone()); + } + } + replacement_func.attrs = other_attrs; +println!("GOAT func={replacement_func:?}"); +println!("GOAT spec={spec:?}"); + }, + _ => {} + } + } + Ok(the_trait).map(|tokens| tokens.into_token_stream()) + }, unsupported_item => { let item_type = item_to_string(&unsupported_item); let msg = format!( @@ -57,7 +89,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(), } } From f0b851fb34e3c3cfd8c4da7d8a637357cbab05ff Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 00:07:34 -0700 Subject: [PATCH 02/20] Removing spec invocations from function items within trait definitions --- crates/anodized/src/lib.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index f4376d2..a9acd30 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -49,13 +49,15 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { // items within the trait let _spec = parse_macro_input!(args as Spec); - for item in &the_trait.items { + let mut replacement_trait = the_trait.clone(); + + //Deal with spec macro markup on items within the trait + for item in replacement_trait.items.iter_mut() { match item { TraitItem::Fn(func) => { - let mut replacement_func = func.clone(); let mut spec = None; let mut other_attrs = Vec::new(); - for attr in &func.attrs { + for attr in core::mem::take(&mut func.attrs) { if attr.path().is_ident("spec") { match attr.parse_args::() { Ok(parsed) => { @@ -64,17 +66,17 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { Err(e) => return e.to_compile_error().into() } } else { - other_attrs.push(attr.clone()); + other_attrs.push(attr); } } - replacement_func.attrs = other_attrs; -println!("GOAT func={replacement_func:?}"); + func.attrs = other_attrs; +println!("GOAT func={func:?}"); println!("GOAT spec={spec:?}"); }, _ => {} } } - Ok(the_trait).map(|tokens| tokens.into_token_stream()) + Ok(replacement_trait).map(|tokens| tokens.into_token_stream()) }, unsupported_item => { let item_type = item_to_string(&unsupported_item); From f9147a965e1c3c7e9b022b6818c7d62c64a0f071 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 20:50:58 -0700 Subject: [PATCH 03/20] Short comments describing trait fn mangling --- crates/anodized/Cargo.toml | 1 + crates/anodized/src/lib.rs | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index 8063b5d..cfd9a19 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 a9acd30..7e0d6ed 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -45,7 +45,17 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { BACKEND.instrument_fn(spec, func).map(|tokens| tokens.into_token_stream()) }, Item::Trait(the_trait) => { - //Currently we don't support any markup for traits themselves - only the + // Mangling a function involves the following: + // + // 1. Rename the function following the pattern: `fn add` would be mangled to `fn __anodized_add` + // 2. Make a new function with a with the original name that has a default impl; the default impl will + // perform all runtime validation, and call through to the mangled function. + // + // Every function in a trait gets mangled, regardless of whether or not it has a [spec] decorator or not. + // This is because the impl has no way of knowing if there is a spec or not on the trait item. + + + //Currently we don't support any spec arguments for traits themselves - only for the // items within the trait let _spec = parse_macro_input!(args as Spec); From a3337323fb6b1ff32cd107dfd0839eb8c91c6e15 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 21:19:04 -0700 Subject: [PATCH 04/20] Adding test for trait specs --- crates/anodized/tests/basic_traits.rs | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 crates/anodized/tests/basic_traits.rs diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs new file mode 100644 index 0000000..bceb21a --- /dev/null +++ b/crates/anodized/tests/basic_traits.rs @@ -0,0 +1,39 @@ + +use anodized::spec; + +#[spec] +pub trait TestTrait { + + fn current(&self) -> u32; + + #[spec( + requires: { + // Just a longer way of writing `true` :) + let x = 5; + x > 0 + }, + captures: { + self.current() + } as old_val, + ensures: { + let new_val = self.current(); + new_val > old_val + }, + )] + fn do_something(&self, x: u32) -> u32; +} + +struct TestStruct; + +impl TestTrait for TestStruct { + fn do_something(&self, x: u32) -> u32 { + x * 2 + } +} + +#[test] +fn basic_trait_test() { + let test = TestStruct; + + assert_eq!(test.do_something(500), 1000); +} \ No newline at end of file From cd25fa49f38e50c5a14964da8a1b91ac9b0376b9 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 21:51:47 -0700 Subject: [PATCH 05/20] Adding trait function mangling. Impl mangling still needed --- crates/anodized/src/lib.rs | 89 ++++++++++++++++++++++++--- crates/anodized/tests/basic_traits.rs | 16 +++-- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index 7e0d6ed..46933a2 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -1,8 +1,9 @@ #![doc = include_str!("../README.md")] use proc_macro::TokenStream; -use quote::ToTokens; -use syn::{Item, TraitItem, parse_macro_input}; +use proc_macro2::Span; +use quote::{ToTokens, quote}; +use syn::{FnArg, Item, ItemFn, Pat, TraitItem, parse_macro_input, parse_quote}; use anodized_core::{Spec, instrument::Backend}; @@ -60,11 +61,12 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { let _spec = parse_macro_input!(args as Spec); let mut replacement_trait = the_trait.clone(); + let mut new_trait_items = Vec::with_capacity(the_trait.items.len() * 2); //Deal with spec macro markup on items within the trait - for item in replacement_trait.items.iter_mut() { + for item in replacement_trait.items.into_iter() { match item { - TraitItem::Fn(func) => { + TraitItem::Fn(mut func) => { let mut spec = None; let mut other_attrs = Vec::new(); for attr in core::mem::take(&mut func.attrs) { @@ -79,13 +81,50 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { other_attrs.push(attr); } } - func.attrs = other_attrs; -println!("GOAT func={func:?}"); -println!("GOAT spec={spec:?}"); + func.attrs = other_attrs.clone(); + + let original_ident = func.sig.ident.clone(); + let mangled_ident = syn::Ident::new( + &format!("__anodized_{original_ident}"), + Span::mixed_site(), + ); + + let mut mangled_fn = func.clone(); + mangled_fn.sig.ident = mangled_ident.clone(); + + let call_args = match build_call_args(&func.sig.inputs) { + Ok(call_args) => call_args, + Err(e) => return e.to_compile_error().into() + }; + let mut wrapper_block: syn::Block = parse_quote!({ + Self::#mangled_ident(#(#call_args),*) + }); + + if let Some(spec) = spec { + let wrapper_item = ItemFn { + attrs: Vec::new(), + vis: syn::Visibility::Inherited, + sig: func.sig.clone(), + block: Box::new(wrapper_block), + }; + match BACKEND.instrument_fn(spec, wrapper_item) { + Ok(instrumented) => {wrapper_block = *instrumented.block;}, + Err(e) => return e.to_compile_error().into() + } + } + + 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), } } + replacement_trait.items = new_trait_items; Ok(replacement_trait).map(|tokens| tokens.into_token_stream()) }, unsupported_item => { @@ -106,6 +145,40 @@ request at https://github.com/mkovaxx/anodized/issues/new"#, } } +/// 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` +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) +} + fn item_to_string(item: &Item) -> &str { match item { Item::Const(_) => "const", diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index bceb21a..3c71063 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -8,16 +8,13 @@ pub trait TestTrait { #[spec( requires: { - // Just a longer way of writing `true` :) - let x = 5; x > 0 }, captures: { self.current() } as old_val, ensures: { - let new_val = self.current(); - new_val > old_val + *output > old_val }, )] fn do_something(&self, x: u32) -> u32; @@ -26,7 +23,16 @@ pub trait TestTrait { struct TestStruct; impl TestTrait for TestStruct { - fn do_something(&self, x: u32) -> u32 { + // fn current(&self) -> u32 { + // 0 + // } + fn __anodized_current(&self) -> u32 { + 0 + } + // fn do_something(&self, x: u32) -> u32 { + // x * 2 + // } + fn __anodized_do_something(&self, x: u32) -> u32 { x * 2 } } From 8aa724af285b343641f445eba7073c721c9ddbfe Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 22:06:02 -0700 Subject: [PATCH 06/20] refactoring trait logic into its own module --- crates/anodized/src/lib.rs | 124 ++---------------------------- crates/anodized/src/trait_spec.rs | 121 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 118 deletions(-) create mode 100644 crates/anodized/src/trait_spec.rs diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index 46933a2..0c5ff44 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -1,11 +1,11 @@ #![doc = include_str!("../README.md")] use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::{ToTokens, quote}; -use syn::{FnArg, Item, ItemFn, Pat, TraitItem, parse_macro_input, parse_quote}; +use quote::ToTokens; +use syn::{Item, parse_macro_input}; use anodized_core::{Spec, instrument::Backend}; +mod trait_spec; const _: () = { let count: u32 = cfg!(feature = "runtime-check-and-panic") as u32 @@ -16,7 +16,7 @@ const _: () = { } }; -const BACKEND: Backend = if cfg!(feature = "runtime-check-and-panic") { +pub(crate) const BACKEND: Backend = if cfg!(feature = "runtime-check-and-panic") { Backend::CHECK_AND_PANIC } else if cfg!(feature = "runtime-check-and-print") { Backend::CHECK_AND_PRINT @@ -46,86 +46,8 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { BACKEND.instrument_fn(spec, func).map(|tokens| tokens.into_token_stream()) }, Item::Trait(the_trait) => { - // Mangling a function involves the following: - // - // 1. Rename the function following the pattern: `fn add` would be mangled to `fn __anodized_add` - // 2. Make a new function with a with the original name that has a default impl; the default impl will - // perform all runtime validation, and call through to the mangled function. - // - // Every function in a trait gets mangled, regardless of whether or not it has a [spec] decorator or not. - // This is because the impl has no way of knowing if there is a spec or not on the trait item. - - - //Currently we don't support any spec arguments for traits themselves - only for the - // items within the trait - let _spec = parse_macro_input!(args as Spec); - - let mut replacement_trait = the_trait.clone(); - let mut new_trait_items = Vec::with_capacity(the_trait.items.len() * 2); - - //Deal with spec macro markup on items within the trait - for item in replacement_trait.items.into_iter() { - match item { - TraitItem::Fn(mut func) => { - let mut spec = None; - let mut other_attrs = Vec::new(); - for attr in core::mem::take(&mut func.attrs) { - if attr.path().is_ident("spec") { - match attr.parse_args::() { - Ok(parsed) => { - spec = Some(parsed) - }, - Err(e) => return e.to_compile_error().into() - } - } else { - other_attrs.push(attr); - } - } - func.attrs = other_attrs.clone(); - - let original_ident = func.sig.ident.clone(); - let mangled_ident = syn::Ident::new( - &format!("__anodized_{original_ident}"), - Span::mixed_site(), - ); - - let mut mangled_fn = func.clone(); - mangled_fn.sig.ident = mangled_ident.clone(); - - let call_args = match build_call_args(&func.sig.inputs) { - Ok(call_args) => call_args, - Err(e) => return e.to_compile_error().into() - }; - let mut wrapper_block: syn::Block = parse_quote!({ - Self::#mangled_ident(#(#call_args),*) - }); - - if let Some(spec) = spec { - let wrapper_item = ItemFn { - attrs: Vec::new(), - vis: syn::Visibility::Inherited, - sig: func.sig.clone(), - block: Box::new(wrapper_block), - }; - match BACKEND.instrument_fn(spec, wrapper_item) { - Ok(instrumented) => {wrapper_block = *instrumented.block;}, - Err(e) => return e.to_compile_error().into() - } - } - - 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), - } - } - replacement_trait.items = new_trait_items; - Ok(replacement_trait).map(|tokens| tokens.into_token_stream()) + trait_spec::instrument_trait(args, the_trait) + .map(|tokens| tokens.into_token_stream()) }, unsupported_item => { let item_type = item_to_string(&unsupported_item); @@ -145,40 +67,6 @@ request at https://github.com/mkovaxx/anodized/issues/new"#, } } -/// 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` -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) -} - fn item_to_string(item: &Item) -> &str { match item { Item::Const(_) => "const", diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs new file mode 100644 index 0000000..1980ba5 --- /dev/null +++ b/crates/anodized/src/trait_spec.rs @@ -0,0 +1,121 @@ +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{FnArg, ItemFn, Pat, TraitItem, parse_quote}; +use crate::BACKEND; + +use anodized_core::Spec; + +/// 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( + args: TokenStream, + mut the_trait: syn::ItemTrait +) -> syn::Result { + // Currently we don't support any spec arguments for traits themselves. + let _spec: Spec = syn::parse(args)?; + + 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 mut spec = None; + let mut other_attrs = Vec::new(); + for attr in core::mem::take(&mut func.attrs) { + if attr.path().is_ident("spec") { + match attr.parse_args::() { + Ok(parsed) => { + spec = Some(parsed); + } + Err(e) => return Err(e), + } + } else { + other_attrs.push(attr); + } + } + func.attrs = other_attrs.clone(); + + let original_ident = func.sig.ident.clone(); + let mangled_ident = syn::Ident::new( + &format!("__anodized_{original_ident}"), + Span::mixed_site(), + ); + + let mut mangled_fn = func.clone(); + mangled_fn.sig.ident = mangled_ident.clone(); + + 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 { + let wrapper_item = ItemFn { + attrs: Vec::new(), + vis: syn::Visibility::Inherited, + sig: func.sig.clone(), + block: Box::new(wrapper_block), + }; + let instrumented = BACKEND.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) +} + +/// 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) +} From fa0b36fb50a8c991ff92bd7779b762e0afc1e6d3 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 22:22:40 -0700 Subject: [PATCH 07/20] Adding impl [spec] handling --- crates/anodized/src/lib.rs | 4 ++ crates/anodized/src/trait_spec.rs | 55 ++++++++++++++++++++++++--- crates/anodized/tests/basic_traits.rs | 11 ++---- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index 0c5ff44..5335c79 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -49,6 +49,10 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { trait_spec::instrument_trait(args, the_trait) .map(|tokens| tokens.into_token_stream()) }, + Item::Impl(the_impl) => { + trait_spec::instrument_impl(args, the_impl) + .map(|tokens| tokens.into_token_stream()) + }, unsupported_item => { let item_type = item_to_string(&unsupported_item); let msg = format!( diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 1980ba5..12a497c 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -1,7 +1,6 @@ use proc_macro::TokenStream; -use proc_macro2::Span; use quote::quote; -use syn::{FnArg, ItemFn, Pat, TraitItem, parse_quote}; +use syn::{FnArg, ImplItem, ItemFn, Pat, TraitItem, parse_quote}; use crate::BACKEND; use anodized_core::Spec; @@ -41,10 +40,7 @@ pub fn instrument_trait( func.attrs = other_attrs.clone(); let original_ident = func.sig.ident.clone(); - let mangled_ident = syn::Ident::new( - &format!("__anodized_{original_ident}"), - Span::mixed_site(), - ); + let mangled_ident = mangle_ident(&original_ident); let mut mangled_fn = func.clone(); mangled_fn.sig.ident = mangled_ident.clone(); @@ -80,6 +76,44 @@ pub fn instrument_trait( Ok(the_trait) } +/// Expand impl items by mangling methods for trait impls. +/// +/// The `#[spec]` on the impl itself is accepted for symmetry with other items, +/// but currently has no effect beyond validation. +pub fn instrument_impl( + args: TokenStream, + mut the_impl: syn::ItemImpl, +) -> syn::Result { + // Currently we don't support any spec arguments for impl blocks themselves. + let _spec: Spec = syn::parse(args)?; + + 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 original_ident = func.sig.ident.clone(); + if !original_ident.to_string().starts_with("__anodized_") { + func.sig.ident = mangle_ident(&original_ident); + } + + 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 @@ -119,3 +153,12 @@ fn build_call_args( } 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(), + ) +} diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index 3c71063..36e7ff6 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -22,17 +22,12 @@ pub trait TestTrait { struct TestStruct; +#[spec] impl TestTrait for TestStruct { - // fn current(&self) -> u32 { - // 0 - // } - fn __anodized_current(&self) -> u32 { + fn current(&self) -> u32 { 0 } - // fn do_something(&self, x: u32) -> u32 { - // x * 2 - // } - fn __anodized_do_something(&self, x: u32) -> u32 { + fn do_something(&self, x: u32) -> u32 { x * 2 } } From 4cc49a75cabea4532bc2f84fb5db45f1dff2062c Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 22:33:00 -0700 Subject: [PATCH 08/20] Updating test to also test specs on the implementation as well as on the trait definition, and to test default method implementations --- crates/anodized/tests/basic_traits.rs | 33 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index 36e7ff6..bd780e1 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -17,24 +17,47 @@ pub trait TestTrait { *output > old_val }, )] - fn do_something(&self, x: u32) -> u32; + fn do_something(&self, x: u32) -> u32 { + x * 2 + } } -struct TestStruct; +struct TestStruct(u32); #[spec] impl TestTrait for TestStruct { fn current(&self) -> u32 { - 0 + self.0 } + #[spec( + maintains: { + self.0 == 3 + }, + ensures: { + *output > self.0 + }, + )] fn do_something(&self, x: u32) -> u32 { - x * 2 + x * self.0 + } +} + +struct TestStructConst; + +#[spec] +impl TestTrait for TestStructConst { + fn current(&self) -> u32 { + 0 } } #[test] fn basic_trait_test() { - let test = TestStruct; + // 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); } \ No newline at end of file From bdf6b3d9b8864a1e569463f07f1ba07d2d30b163 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 23:30:48 -0700 Subject: [PATCH 09/20] Adding `#[doc(hidden)]` and `#[inline]` attribs in the appropriate places in the output --- crates/anodized/src/trait_spec.rs | 62 ++++++++++++++++++++------- crates/anodized/tests/basic_traits.rs | 1 + 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 12a497c..61f3c35 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{FnArg, ImplItem, ItemFn, Pat, TraitItem, parse_quote}; +use syn::{Attribute, FnArg, ImplItem, ItemFn, Pat, TraitItem, parse_quote}; use crate::BACKEND; use anodized_core::Spec; @@ -23,20 +23,12 @@ pub fn instrument_trait( for item in the_trait.items.into_iter() { match item { TraitItem::Fn(mut func) => { - let mut spec = None; - let mut other_attrs = Vec::new(); - for attr in core::mem::take(&mut func.attrs) { - if attr.path().is_ident("spec") { - match attr.parse_args::() { - Ok(parsed) => { - spec = Some(parsed); - } - Err(e) => return Err(e), - } - } else { - other_attrs.push(attr); - } - } + let (spec, other_attrs) = parse_spec_attrs(func.attrs, true)?; + + //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(); @@ -44,6 +36,7 @@ pub fn instrument_trait( let mut mangled_fn = func.clone(); mangled_fn.sig.ident = mangled_ident.clone(); + 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!({ @@ -104,6 +97,12 @@ pub fn instrument_impl( 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])); + } + new_items.push(ImplItem::Fn(func)); } other => new_items.push(other), @@ -162,3 +161,36 @@ fn mangle_ident(original_ident: &syn::Ident) -> syn::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 +fn parse_spec_attrs( + attrs: Vec, + remove: bool, +) -> syn::Result<(Option, Vec)> { + let mut spec: Option = 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::()?); + if !remove { + other_attrs.push(attr); + } + } else { + other_attrs.push(attr); + } + } + + Ok((spec, other_attrs)) +} diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index bd780e1..9e947f0 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -37,6 +37,7 @@ impl TestTrait for TestStruct { *output > self.0 }, )] + #[inline(never)] fn do_something(&self, x: u32) -> u32 { x * self.0 } From 3c25d51279ad813fd2563f4303297d2984fae9f7 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 23:36:36 -0700 Subject: [PATCH 10/20] Removing unnecessary docs (which are always hidden) from the emitted output --- crates/anodized/src/trait_spec.rs | 1 + crates/anodized/tests/basic_traits.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 61f3c35..755d470 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -36,6 +36,7 @@ pub fn instrument_trait( 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)?; diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index 9e947f0..a514536 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -4,9 +4,11 @@ use anodized::spec; #[spec] pub trait TestTrait { + /// Returns a current value fn current(&self) -> u32; - #[spec( + /// Does something + #[spec{ requires: { x > 0 }, @@ -16,7 +18,7 @@ pub trait TestTrait { ensures: { *output > old_val }, - )] + }] fn do_something(&self, x: u32) -> u32 { x * 2 } @@ -29,14 +31,14 @@ impl TestTrait for TestStruct { fn current(&self) -> u32 { self.0 } - #[spec( + #[spec{ maintains: { self.0 == 3 }, ensures: { *output > self.0 }, - )] + }] #[inline(never)] fn do_something(&self, x: u32) -> u32 { x * self.0 From 53c76b2c8c1c9ffc344a79adc28afc3e15e978c2 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 11 Jan 2026 23:48:18 -0700 Subject: [PATCH 11/20] Reverting default runtime behavior feature --- crates/anodized/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index cfd9a19..e75645a 100644 --- a/crates/anodized/Cargo.toml +++ b/crates/anodized/Cargo.toml @@ -14,7 +14,7 @@ license.workspace = true proc-macro = true [features] -default = ["runtime-check-and-print"] +# default = ["runtime-check-and-print"] runtime-check-and-panic = [] runtime-check-and-print = [] runtime-no-check = [] From 35b7ede76ec1e0407a64c182243f6de609dc7442 Mon Sep 17 00:00:00 2001 From: luketpeterson <36806965+luketpeterson@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:00:16 -0700 Subject: [PATCH 12/20] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tweaks from code review (mostly format and renames) Co-authored-by: Máté Kovács <481354+mkovaxx@users.noreply.github.com> --- crates/anodized/src/lib.rs | 4 ++-- crates/anodized/src/trait_spec.rs | 2 +- crates/anodized/tests/basic_traits.rs | 30 +++++++++------------------ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/crates/anodized/src/lib.rs b/crates/anodized/src/lib.rs index 5335c79..5475a22 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -16,7 +16,7 @@ const _: () = { } }; -pub(crate) const BACKEND: Backend = if cfg!(feature = "runtime-check-and-panic") { +const BACKEND: Backend = if cfg!(feature = "runtime-check-and-panic") { Backend::CHECK_AND_PANIC } else if cfg!(feature = "runtime-check-and-print") { Backend::CHECK_AND_PRINT @@ -49,7 +49,7 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { trait_spec::instrument_trait(args, the_trait) .map(|tokens| tokens.into_token_stream()) }, - Item::Impl(the_impl) => { + Item::Impl(the_impl) if the_impl.trait_.is_some() => { trait_spec::instrument_impl(args, the_impl) .map(|tokens| tokens.into_token_stream()) }, diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 755d470..890ea6e 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -74,7 +74,7 @@ pub fn instrument_trait( /// /// The `#[spec]` on the impl itself is accepted for symmetry with other items, /// but currently has no effect beyond validation. -pub fn instrument_impl( +pub fn instrument_trait_impl( args: TokenStream, mut the_impl: syn::ItemImpl, ) -> syn::Result { diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index a514536..62d6b47 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -8,17 +8,11 @@ pub trait TestTrait { fn current(&self) -> u32; /// Does something - #[spec{ - requires: { - x > 0 - }, - captures: { - self.current() - } as old_val, - ensures: { - *output > old_val - }, - }] + #[spec( + requires: x > 0, + captures: self.current() as old_val, + ensures: *output > old_val, + )] fn do_something(&self, x: u32) -> u32 { x * 2 } @@ -31,14 +25,10 @@ impl TestTrait for TestStruct { fn current(&self) -> u32 { self.0 } - #[spec{ - maintains: { - self.0 == 3 - }, - ensures: { - *output > self.0 - }, - }] + #[spec( + maintains: self.0 == 3, + ensures: *output > self.0, + )] #[inline(never)] fn do_something(&self, x: u32) -> u32 { x * self.0 @@ -63,4 +53,4 @@ fn basic_trait_test() { // Tests a default method implementation coming from a trait let test = TestStructConst; assert_eq!(test.do_something(500), 1000); -} \ No newline at end of file +} From 00f9d3de4d86eda0a2cf6c6e15171de6530c4947 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 12 Jan 2026 21:06:16 -0700 Subject: [PATCH 13/20] Fixing rename missed by previous commit --- crates/anodized/src/trait_spec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 890ea6e..755d470 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -74,7 +74,7 @@ pub fn instrument_trait( /// /// The `#[spec]` on the impl itself is accepted for symmetry with other items, /// but currently has no effect beyond validation. -pub fn instrument_trait_impl( +pub fn instrument_impl( args: TokenStream, mut the_impl: syn::ItemImpl, ) -> syn::Result { From 770c30b1f7ab2f472a03eb09fe9b4198ce703270 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 12 Jan 2026 23:13:51 -0700 Subject: [PATCH 14/20] Throwing error when spec element is supplied on top-level spec attribute on trait or impl block --- crates/anodized-core/src/lib.rs | 7 +++++++ crates/anodized/src/trait_spec.rs | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/anodized-core/src/lib.rs b/crates/anodized-core/src/lib.rs index c0899bb..0b0b1e2 100644 --- a/crates/anodized-core/src/lib.rs +++ b/crates/anodized-core/src/lib.rs @@ -21,6 +21,13 @@ pub struct Spec { pub ensures: Vec, } +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() + } +} + /// A condition represented by a `bool`-valued expression. #[derive(Debug)] pub struct Condition { diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 755d470..630010a 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -16,7 +16,13 @@ pub fn instrument_trait( mut the_trait: syn::ItemTrait ) -> syn::Result { // Currently we don't support any spec arguments for traits themselves. - let _spec: Spec = syn::parse(args)?; + let spec: Spec = syn::parse(args.clone())?; + if !spec.is_empty() { + return Err(syn::Error::new_spanned::( + args.into(), + "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); @@ -78,8 +84,13 @@ pub fn instrument_impl( args: TokenStream, mut the_impl: syn::ItemImpl, ) -> syn::Result { - // Currently we don't support any spec arguments for impl blocks themselves. - let _spec: Spec = syn::parse(args)?; + let spec: Spec = syn::parse(args.clone())?; + if !spec.is_empty() { + return Err(syn::Error::new_spanned::( + args.into(), + "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( From d116cbff358ea1d6994c438f9565083c2e75305d Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 16 Jan 2026 22:18:56 -0700 Subject: [PATCH 15/20] Making it an error if a trait function impl has a spec that contains a `requires`, `maintains`, or `ensures` directive --- crates/anodized/Cargo.toml | 2 +- crates/anodized/src/trait_spec.rs | 42 +++++++++++++++++---------- crates/anodized/tests/basic_traits.rs | 9 +++--- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index e75645a..cfd9a19 100644 --- a/crates/anodized/Cargo.toml +++ b/crates/anodized/Cargo.toml @@ -14,7 +14,7 @@ license.workspace = true proc-macro = true [features] -# default = ["runtime-check-and-print"] +default = ["runtime-check-and-print"] runtime-check-and-panic = [] runtime-check-and-print = [] runtime-no-check = [] diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 630010a..20e9645 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -29,7 +29,7 @@ pub fn instrument_trait( for item in the_trait.items.into_iter() { match item { TraitItem::Fn(mut func) => { - let (spec, other_attrs) = parse_spec_attrs(func.attrs, true)?; + 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 @@ -50,7 +50,7 @@ pub fn instrument_trait( Self::#mangled_ident(#(#call_args),*) }); - if let Some(spec) = spec { + if let Some((spec, _spec_attr)) = spec { let wrapper_item = ItemFn { attrs: Vec::new(), vis: syn::Visibility::Inherited, @@ -76,10 +76,9 @@ pub fn instrument_trait( Ok(the_trait) } -/// Expand impl items by mangling methods for trait impls. +/// Expand impl items by mangling methods for trait impls /// -/// The `#[spec]` on the impl itself is accepted for symmetry with other items, -/// but currently has no effect beyond validation. +/// `#[spec]` attributes on the impl items themselves may not have `requires`, `maintains`, nor `ensures` directives. pub fn instrument_impl( args: TokenStream, mut the_impl: syn::ItemImpl, @@ -104,6 +103,18 @@ pub fn instrument_impl( 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, spec_attr)) = spec { + 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); @@ -111,10 +122,11 @@ pub fn instrument_impl( //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])); + 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), @@ -180,11 +192,12 @@ fn has_inline_attr(attrs: &[syn::Attribute]) -> bool { } /// Parses out the `[spec]` attrib from a function's attribute list -fn parse_spec_attrs( - attrs: Vec, - remove: bool, -) -> syn::Result<(Option, Vec)> { - let mut spec: Option = None; +/// +/// 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 { @@ -195,10 +208,7 @@ fn parse_spec_attrs( "multiple `#[spec]` attributes on a single method are not supported", )); } - spec = Some(attr.parse_args::()?); - if !remove { - other_attrs.push(attr); - } + spec = Some((attr.parse_args::()?, attr)); } else { other_attrs.push(attr); } diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index 62d6b47..0f723a3 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -25,10 +25,11 @@ impl TestTrait for TestStruct { fn current(&self) -> u32 { self.0 } - #[spec( - maintains: self.0 == 3, - ensures: *output > self.0, - )] + #[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 From ae1784dfcef8eda5ff0493e7daf17cd3e02dbe6b Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 16 Jan 2026 23:28:04 -0700 Subject: [PATCH 16/20] Oops. Added default cargo feature in the last commit --- crates/anodized/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index cfd9a19..e75645a 100644 --- a/crates/anodized/Cargo.toml +++ b/crates/anodized/Cargo.toml @@ -14,7 +14,7 @@ license.workspace = true proc-macro = true [features] -default = ["runtime-check-and-print"] +# default = ["runtime-check-and-print"] runtime-check-and-panic = [] runtime-check-and-print = [] runtime-no-check = [] From 439f9002808a23d92f16b511789da269f0dd1576 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sat, 17 Jan 2026 00:33:18 -0700 Subject: [PATCH 17/20] Making #[spec] attribs on impl functions illegal --- crates/anodized/src/trait_spec.rs | 23 +++++++++++++++-------- crates/anodized/tests/basic_traits.rs | 11 ++++++----- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs index 20e9645..d787ee7 100644 --- a/crates/anodized/src/trait_spec.rs +++ b/crates/anodized/src/trait_spec.rs @@ -105,14 +105,21 @@ pub fn instrument_impl( ImplItem::Fn(mut func) => { let (spec, mut func_attrs) = parse_spec_attr(func.attrs)?; - if let Some((spec, spec_attr)) = spec { - 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); + 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(); diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index 0f723a3..e23e92d 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -25,11 +25,12 @@ impl TestTrait for TestStruct { fn current(&self) -> u32 { self.0 } - #[spec{ - //gOAT, spec is allowed, but it must not have a `requires`, `maintains`, nor `ensures` directive - // maintains: self.0 == 3, - // ensures: *output > 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 From a1340e4b7c2e2dd5e1b4a177f82ed442aba69492 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sat, 17 Jan 2026 01:10:59 -0700 Subject: [PATCH 18/20] Moving trait and impl-block code to anodized-core Adding span to Spec type --- crates/anodized-core/src/annotate/mod.rs | 1 + crates/anodized-core/src/annotate/tests.rs | 20 ++ .../src/instrument/function/mod.rs | 4 +- crates/anodized-core/src/instrument/mod.rs | 2 + .../src/instrument/trait_spec.rs | 219 +++++++++++++++++ crates/anodized-core/src/lib.rs | 7 + crates/anodized-core/src/test_util.rs | 2 + crates/anodized/src/lib.rs | 7 +- crates/anodized/src/trait_spec.rs | 225 ------------------ 9 files changed, 257 insertions(+), 230 deletions(-) create mode 100644 crates/anodized-core/src/instrument/trait_spec.rs delete mode 100644 crates/anodized/src/trait_spec.rs 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..d142f96 100644 --- a/crates/anodized-core/src/annotate/tests.rs +++ b/crates/anodized-core/src/annotate/tests.rs @@ -2,6 +2,7 @@ use crate::test_util::assert_spec_eq; use super::*; use syn::parse_quote; +use proc_macro2::Span; #[test] fn simple_spec() { @@ -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..083aff6 100644 --- a/crates/anodized-core/src/instrument/mod.rs +++ b/crates/anodized-core/src/instrument/mod.rs @@ -3,6 +3,8 @@ 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.rs b/crates/anodized-core/src/instrument/trait_spec.rs new file mode 100644 index 0000000..cb690f9 --- /dev/null +++ b/crates/anodized-core/src/instrument/trait_spec.rs @@ -0,0 +1,219 @@ +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_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 0b0b1e2..0209825 100644 --- a/crates/anodized-core/src/lib.rs +++ b/crates/anodized-core/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../README.md")] use syn::{Expr, Ident, Meta}; +use proc_macro2::Span; pub mod annotate; pub mod instrument; @@ -19,6 +20,8 @@ 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 { @@ -26,6 +29,10 @@ impl Spec { 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/src/lib.rs b/crates/anodized/src/lib.rs index 5475a22..aa0cdf3 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -5,7 +5,6 @@ use quote::ToTokens; use syn::{Item, parse_macro_input}; use anodized_core::{Spec, instrument::Backend}; -mod trait_spec; const _: () = { let count: u32 = cfg!(feature = "runtime-check-and-panic") as u32 @@ -46,11 +45,13 @@ pub fn spec(args: TokenStream, input: TokenStream) -> TokenStream { BACKEND.instrument_fn(spec, func).map(|tokens| tokens.into_token_stream()) }, Item::Trait(the_trait) => { - trait_spec::instrument_trait(args, 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() => { - trait_spec::instrument_impl(args, the_impl) + let spec = parse_macro_input!(args as Spec); + BACKEND.instrument_impl(spec, the_impl) .map(|tokens| tokens.into_token_stream()) }, unsupported_item => { diff --git a/crates/anodized/src/trait_spec.rs b/crates/anodized/src/trait_spec.rs deleted file mode 100644 index d787ee7..0000000 --- a/crates/anodized/src/trait_spec.rs +++ /dev/null @@ -1,225 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{Attribute, FnArg, ImplItem, ItemFn, Pat, TraitItem, parse_quote}; -use crate::BACKEND; - -use anodized_core::Spec; - -/// 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( - args: TokenStream, - mut the_trait: syn::ItemTrait -) -> syn::Result { - // Currently we don't support any spec arguments for traits themselves. - let spec: Spec = syn::parse(args.clone())?; - if !spec.is_empty() { - return Err(syn::Error::new_spanned::( - args.into(), - "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 = BACKEND.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_impl( - args: TokenStream, - mut the_impl: syn::ItemImpl, -) -> syn::Result { - let spec: Spec = syn::parse(args.clone())?; - if !spec.is_empty() { - return Err(syn::Error::new_spanned::( - args.into(), - "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)) -} From e74bdee4b84594122ed564c84dc19c7c0de240bd Mon Sep 17 00:00:00 2001 From: Mate Kovacs Date: Mon, 19 Jan 2026 22:06:21 +0900 Subject: [PATCH 19/20] rearrange and fmt --- crates/anodized-core/src/annotate/tests.rs | 2 +- crates/anodized-core/src/instrument/mod.rs | 2 +- .../{trait_spec.rs => trait_spec/mod.rs} | 7 +++---- crates/anodized-core/src/lib.rs | 7 +++++-- crates/anodized/Cargo.toml | 2 +- crates/anodized/src/lib.rs | 16 ++++++++++------ crates/anodized/tests/basic_traits.rs | 2 -- 7 files changed, 21 insertions(+), 17 deletions(-) rename crates/anodized-core/src/instrument/{trait_spec.rs => trait_spec/mod.rs} (98%) diff --git a/crates/anodized-core/src/annotate/tests.rs b/crates/anodized-core/src/annotate/tests.rs index d142f96..2fdafc9 100644 --- a/crates/anodized-core/src/annotate/tests.rs +++ b/crates/anodized-core/src/annotate/tests.rs @@ -1,8 +1,8 @@ use crate::test_util::assert_spec_eq; use super::*; -use syn::parse_quote; use proc_macro2::Span; +use syn::parse_quote; #[test] fn simple_spec() { diff --git a/crates/anodized-core/src/instrument/mod.rs b/crates/anodized-core/src/instrument/mod.rs index 083aff6..a187b01 100644 --- a/crates/anodized-core/src/instrument/mod.rs +++ b/crates/anodized-core/src/instrument/mod.rs @@ -1,8 +1,8 @@ use proc_macro2::TokenStream; use quote::quote; use syn::Meta; -pub mod function; +pub mod function; pub mod trait_spec; pub struct Backend { diff --git a/crates/anodized-core/src/instrument/trait_spec.rs b/crates/anodized-core/src/instrument/trait_spec/mod.rs similarity index 98% rename from crates/anodized-core/src/instrument/trait_spec.rs rename to crates/anodized-core/src/instrument/trait_spec/mod.rs index cb690f9..51c69a2 100644 --- a/crates/anodized-core/src/instrument/trait_spec.rs +++ b/crates/anodized-core/src/instrument/trait_spec/mod.rs @@ -13,7 +13,7 @@ impl Backend { pub fn instrument_trait( &self, spec: Spec, - mut the_trait: syn::ItemTrait + mut the_trait: syn::ItemTrait, ) -> syn::Result { // Currently we don't support any spec arguments for traits themselves. if !spec.is_empty() { @@ -75,7 +75,7 @@ impl Backend { /// 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_impl( + pub fn instrument_trait_impl( &self, spec: Spec, mut the_impl: syn::ItemImpl, @@ -96,7 +96,6 @@ impl Backend { 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( @@ -196,7 +195,7 @@ fn has_inline_attr(attrs: &[syn::Attribute]) -> bool { /// /// Returns the parsed spec, the spec [Attribute] and the remaining attributes fn parse_spec_attr( - attrs: Vec + attrs: Vec, ) -> syn::Result<(Option<(Spec, Attribute)>, Vec)> { let mut spec = None; let mut other_attrs = Vec::new(); diff --git a/crates/anodized-core/src/lib.rs b/crates/anodized-core/src/lib.rs index 0209825..17ed27a 100644 --- a/crates/anodized-core/src/lib.rs +++ b/crates/anodized-core/src/lib.rs @@ -1,7 +1,7 @@ #![doc = include_str!("../README.md")] -use syn::{Expr, Ident, Meta}; use proc_macro2::Span; +use syn::{Expr, Ident, Meta}; pub mod annotate; pub mod instrument; @@ -27,7 +27,10 @@ pub struct Spec { 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() + 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 { diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index e75645a..cfd9a19 100644 --- a/crates/anodized/Cargo.toml +++ b/crates/anodized/Cargo.toml @@ -14,7 +14,7 @@ license.workspace = true proc-macro = true [features] -# default = ["runtime-check-and-print"] +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 aa0cdf3..c0c6372 100644 --- a/crates/anodized/src/lib.rs +++ b/crates/anodized/src/lib.rs @@ -42,18 +42,22 @@ 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).map(|tokens| tokens.into_token_stream()) - }, + 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) + 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_impl(spec, the_impl) + BACKEND + .instrument_trait_impl(spec, the_impl) .map(|tokens| tokens.into_token_stream()) - }, + } unsupported_item => { let item_type = item_to_string(&unsupported_item); let msg = format!( diff --git a/crates/anodized/tests/basic_traits.rs b/crates/anodized/tests/basic_traits.rs index e23e92d..f867e4a 100644 --- a/crates/anodized/tests/basic_traits.rs +++ b/crates/anodized/tests/basic_traits.rs @@ -1,9 +1,7 @@ - use anodized::spec; #[spec] pub trait TestTrait { - /// Returns a current value fn current(&self) -> u32; From fdc869ca6b82410fc16d4ce648b0e640073a29de Mon Sep 17 00:00:00 2001 From: Mate Kovacs Date: Mon, 19 Jan 2026 22:11:47 +0900 Subject: [PATCH 20/20] meh --- crates/anodized/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anodized/Cargo.toml b/crates/anodized/Cargo.toml index cfd9a19..e75645a 100644 --- a/crates/anodized/Cargo.toml +++ b/crates/anodized/Cargo.toml @@ -14,7 +14,7 @@ license.workspace = true proc-macro = true [features] -default = ["runtime-check-and-print"] +# default = ["runtime-check-and-print"] runtime-check-and-panic = [] runtime-check-and-print = [] runtime-no-check = []