Skip to main content

ommx_derive/
lib.rs

1//! Derive macros for the `ommx` crate.
2//!
3//! This crate exists solely as an implementation detail of [`ommx`]: it
4//! is published to crates.io because `ommx` depends on it, but it has no
5//! stable API of its own and **no public surface for external use**.
6//!
7//! The `ommx` crate gates the `LogicalMemoryProfile` trait and the
8//! re-exported derive behind a `pub(crate)` module, so downstream users
9//! cannot reach them through the public API and cannot meaningfully
10//! derive on their own types. The trait and the re-export are declared
11//! `pub` *inside* that module to satisfy the `private_bounds` lint when
12//! the trait appears in the bound of a `pub` type within the crate
13//! (e.g. `ConstraintMetadataStore<ID: ... + LogicalMemoryProfile>`).
14//! External consumers should use
15//! [`ommx::Instance::logical_memory_profile`] and
16//! [`ommx::MemoryProfile`] instead.
17//!
18//! [`ommx`]: https://docs.rs/ommx
19//! [`ommx::Instance::logical_memory_profile`]: https://docs.rs/ommx/latest/ommx/struct.Instance.html#method.logical_memory_profile
20//! [`ommx::MemoryProfile`]: https://docs.rs/ommx/latest/ommx/struct.MemoryProfile.html
21//!
22//! # `#[derive(LogicalMemoryProfile)]`
23//!
24//! Generates a `LogicalMemoryProfile` impl that delegates to each field
25//! of a named-field struct. Each field is emitted under the path frame
26//! `"TypeName.field_name"`. The `ommx` crate uses this derive at every
27//! struct definition that participates in memory profiling, so that
28//! adding or removing a field automatically adjusts the profile.
29//!
30//! ## Supported
31//!
32//! - Structs with named fields.
33//!   - All fields must implement `LogicalMemoryProfile`. Primitives,
34//!     `String`, `Option<T>`, `Vec<T>`, `BTreeMap`, `HashMap`,
35//!     `FnvHashMap`, and `BTreeSet` all have blanket impls in
36//!     `ommx::logical_memory::collections`.
37//! - Generic structs: type parameters are propagated through, but
38//!   **no `LogicalMemoryProfile` bound is added automatically**. The
39//!   struct must declare its own `where T: LogicalMemoryProfile`
40//!   clause. This matches `serde`'s historical `#[serde(bound = ...)]`
41//!   philosophy — the derive does not guess.
42//!
43//! ## Not supported
44//!
45//! - Tuple structs and unit structs → emit a `compile_error!` directing
46//!   the caller to a hand-written impl.
47//! - Enums → emit a `compile_error!`. For enums, hand-write a `match`
48//!   (`Function` in the `ommx` crate is an example).
49//! - Field skipping → there is no `#[logical_memory(skip)]` attribute.
50//!   If a field truly should not participate, hand-write the impl.
51//! - Custom frame names → the frame is always `"TypeName.field_name"`
52//!   taken from the struct ident and field ident. For a renamed frame
53//!   (e.g. when wrapping an external type), use the declarative
54//!   `impl_logical_memory_profile! { path::to::Type as "Name" { ... } }`
55//!   form instead.
56//!
57//! # Testing
58//!
59//! The proc-macro entry point delegates to
60//! `derive_logical_memory_profile_impl`, a pure
61//! `TokenStream2 -> TokenStream2` function. This is exercised by inline
62//! `insta` snapshot tests in this crate — the generated code is checked
63//! in as a snapshot so any drift is caught at review time.
64
65use proc_macro::TokenStream;
66use proc_macro2::TokenStream as TokenStream2;
67use quote::quote;
68use syn::{Data, DeriveInput, Fields};
69
70/// Derive `LogicalMemoryProfile` for a struct by delegating to each field.
71///
72/// Only structs with named fields are supported. Each field's profile is
73/// emitted under the path frame `"TypeName.field_name"`.
74#[proc_macro_derive(LogicalMemoryProfile)]
75pub fn derive_logical_memory_profile(input: TokenStream) -> TokenStream {
76    derive_logical_memory_profile_impl(input.into()).into()
77}
78
79/// Pure `TokenStream2` entry point for the derive.
80///
81/// Split out from the `#[proc_macro_derive]` wrapper so that unit tests
82/// can exercise the code-generation logic without the proc-macro runtime.
83fn derive_logical_memory_profile_impl(input: TokenStream2) -> TokenStream2 {
84    let input = match syn::parse2::<DeriveInput>(input) {
85        Ok(ast) => ast,
86        Err(err) => return err.to_compile_error(),
87    };
88    let name = &input.ident;
89    let name_str = name.to_string();
90
91    let fields = match &input.data {
92        Data::Struct(data) => match &data.fields {
93            Fields::Named(fields) => &fields.named,
94            _ => {
95                return syn::Error::new_spanned(
96                    name,
97                    "LogicalMemoryProfile derive only supports structs with named fields",
98                )
99                .to_compile_error();
100            }
101        },
102        _ => {
103            return syn::Error::new_spanned(
104                name,
105                "LogicalMemoryProfile derive only supports structs",
106            )
107            .to_compile_error();
108        }
109    };
110
111    let field_visits = fields.iter().map(|field| {
112        let field_name = field.ident.as_ref().expect("named field");
113        let frame = format!("{name_str}.{field_name}");
114        quote! {
115            ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
116                &self.#field_name,
117                path.with(#frame).as_mut(),
118                visitor,
119            );
120        }
121    });
122
123    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
124
125    quote! {
126        impl #impl_generics ::ommx::logical_memory::LogicalMemoryProfile
127            for #name #ty_generics #where_clause
128        {
129            fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
130                &self,
131                path: &mut ::ommx::logical_memory::Path,
132                visitor: &mut __V,
133            ) {
134                #( #field_visits )*
135            }
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    /// Render a derive-generated `TokenStream2` as a formatted Rust source
145    /// string, so `insta::assert_snapshot!` diffs are readable.
146    fn render(input: TokenStream2) -> String {
147        let tokens = derive_logical_memory_profile_impl(input);
148        let file: syn::File = syn::parse2(tokens).expect("derive output must parse as syn::File");
149        prettyplease::unparse(&file)
150    }
151
152    #[test]
153    fn snapshot_flat_struct() {
154        let input = quote! {
155            struct Foo {
156                a: u64,
157                b: String,
158            }
159        };
160        insta::assert_snapshot!(render(input), @r###"
161        impl ::ommx::logical_memory::LogicalMemoryProfile for Foo {
162            fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
163                &self,
164                path: &mut ::ommx::logical_memory::Path,
165                visitor: &mut __V,
166            ) {
167                ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
168                    &self.a,
169                    path.with("Foo.a").as_mut(),
170                    visitor,
171                );
172                ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
173                    &self.b,
174                    path.with("Foo.b").as_mut(),
175                    visitor,
176                );
177            }
178        }
179        "###);
180    }
181
182    #[test]
183    fn snapshot_single_field_struct() {
184        let input = quote! {
185            struct Wrapper {
186                inner: Inner,
187            }
188        };
189        insta::assert_snapshot!(render(input), @r###"
190        impl ::ommx::logical_memory::LogicalMemoryProfile for Wrapper {
191            fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
192                &self,
193                path: &mut ::ommx::logical_memory::Path,
194                visitor: &mut __V,
195            ) {
196                ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
197                    &self.inner,
198                    path.with("Wrapper.inner").as_mut(),
199                    visitor,
200                );
201            }
202        }
203        "###);
204    }
205
206    #[test]
207    fn snapshot_empty_struct() {
208        // Unit-like structs with empty named-field bodies are legal; the
209        // derive should emit an empty `visit_logical_memory` body.
210        let input = quote! {
211            struct Empty {}
212        };
213        insta::assert_snapshot!(render(input), @r###"
214        impl ::ommx::logical_memory::LogicalMemoryProfile for Empty {
215            fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
216                &self,
217                path: &mut ::ommx::logical_memory::Path,
218                visitor: &mut __V,
219            ) {}
220        }
221        "###);
222    }
223
224    #[test]
225    fn snapshot_generic_struct() {
226        // Generic parameters are propagated without automatic trait-bound
227        // injection; callers must ensure `T: LogicalMemoryProfile` themselves
228        // (e.g. via a `where` clause on the struct definition).
229        let input = quote! {
230            struct Generic<T> where T: ::ommx::logical_memory::LogicalMemoryProfile {
231                value: T,
232                count: u64,
233            }
234        };
235        insta::assert_snapshot!(render(input), @r###"
236        impl<T> ::ommx::logical_memory::LogicalMemoryProfile for Generic<T>
237        where
238            T: ::ommx::logical_memory::LogicalMemoryProfile,
239        {
240            fn visit_logical_memory<__V: ::ommx::logical_memory::LogicalMemoryVisitor>(
241                &self,
242                path: &mut ::ommx::logical_memory::Path,
243                visitor: &mut __V,
244            ) {
245                ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
246                    &self.value,
247                    path.with("Generic.value").as_mut(),
248                    visitor,
249                );
250                ::ommx::logical_memory::LogicalMemoryProfile::visit_logical_memory(
251                    &self.count,
252                    path.with("Generic.count").as_mut(),
253                    visitor,
254                );
255            }
256        }
257        "###);
258    }
259
260    #[test]
261    fn snapshot_rejects_enum() {
262        // Error output is also snapshot-tested to lock in the diagnostic
263        // message surface. The generated compile_error! invocation is the
264        // contract for non-struct inputs.
265        let input = quote! {
266            enum NotSupported { A, B }
267        };
268        insta::assert_snapshot!(render(input), @r###"
269        ::core::compile_error! {
270            "LogicalMemoryProfile derive only supports structs"
271        }
272        "###);
273    }
274
275    #[test]
276    fn snapshot_rejects_tuple_struct() {
277        let input = quote! {
278            struct Tuple(u64, String);
279        };
280        insta::assert_snapshot!(render(input), @r###"
281        ::core::compile_error! {
282            "LogicalMemoryProfile derive only supports structs with named fields"
283        }
284        "###);
285    }
286}